/*
  Styles for the parts of the frequency builder that scheduler.js creates at runtime — chips,
  calendar cells, the date list, the saved-frequency drawer. Everything static is plain portal
  markup in Frequencies.cshtml.

  Colours come from the portal's own theme tokens, so light and dark, and every skin, follow the theme
  without anything here knowing which one is active. This file is hand-written and is not part of
  the Tailwind build, so `gulp build` will not overwrite it.
*/

/* Several rules below set an explicit display, which would otherwise beat [hidden]. */
.page-content [hidden],
.library[hidden] {
  display: none !important;
}

/* ---------- Preview column ---------- */

.scheduler-preview {
  position: sticky;
  top: calc(var(--topbar-height, 70px) + 1rem);
}

@media (max-width: 1279px) {
  .scheduler-preview { position: static; }
}

.scheduler-subhead {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--color-default-400);
  margin-bottom: 0.6rem;
}

.summary {
  font-size: 0.95rem;
  line-height: 1.5;
  margin: 0 0 1rem;
  color: var(--color-default-800);
  text-wrap: pretty;
}

.tally {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.5rem;
  margin: 0 0 1.25rem;
  padding: 0.75rem 0;
  border-block: 1px dashed var(--color-default-300);
}

.tally dt {
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--color-default-400);
}

.tally dd {
  margin: 0.15rem 0 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.85rem;
  color: var(--color-default-800);
}

/* ---------- The pattern clause ---------- */

/*
  One question with a few answers, of which exactly one is chosen — the builder's frequency, the
  queue's whose-queue switch. Marked up as a radiogroup, because that is what it is.

  Columns are laid out as they come rather than fixed at four. The builder has always had four
  equal ones and still does; anything else now works without a second component, which is what
  stopped the queue's switch being one.
*/
.segmented {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  border: 1px solid var(--color-default-300);
  border-radius: 0.375rem;
  overflow: hidden;
  margin-bottom: 1.25rem;
}

.segmented button {
  background: transparent;
  border: 0;
  border-inline-end: 1px solid var(--color-default-300);
  padding: 0.5rem;
  cursor: pointer;
  color: var(--color-default-500);
  font-size: 0.875rem;
  transition: background-color 0.15s ease, color 0.15s ease;
}

.segmented button:last-child { border-inline-end: 0; }
.segmented button:hover { background: var(--color-light); }

/*
  The same control standing in a toolbar rather than in a form.

  After .segmented button, not before it: these override its padding, and a rule of equal
  specificity only wins by coming later. Put above, they lost silently and the control stayed the
  wrong size — which is exactly how it got here.
*/
.segmented--inline {
  /* No trailing margin. In a centred flex row that does not push what is below away, it lifts the
     control itself off the line everything else sits on. */
  margin-bottom: 0;

  /* Its labels' width, rather than every label getting the width of the longest. */
  grid-auto-columns: auto;

  /* Never squeezed by the row beside it. Squeezed, the columns narrow until "To approve" wraps and
     the control is suddenly two lines and sixty pixels tall. */
  flex: none;

  /*
    Stated rather than arrived at. The inputs and buttons in this row are 36 to 37 tall, and
    matching them by adding up padding and line-height is a sum that comes out differently the next
    time any of those change. The buttons inside stretch to fill it.
  */
  min-height: 2.3rem;
}

/*
  w-auto, honoured.

  The theme emits `.w-auto{width:auto}` and, two hundred thousand characters later,
  `.form-select{width:100%}`. Both are one class, so the later one wins and `class="form-select
  w-auto"` has never once made a select narrow. It was invisible while the toolbar was forced onto
  a single line — flex-shrink squeezed the select down and it looked deliberate — and the moment
  the row was allowed to wrap the select took a line to itself at nine hundred pixels wide.

  Two classes, in a stylesheet that loads afterwards, so the markup's intent is what happens.
*/
.form-select.w-auto {
  width: auto;
}

/*
  Matched to the inputs and buttons beside it, which are 36 to 37 pixels tall. A control six taller
  than its neighbours reads as belonging to a different screen, which is most of what was wrong.
*/
.segmented--inline button {
  /* Vertical space comes from the track's height above; this is only the space either side of the
     label, matched to the buttons in the row. */
  padding: 0 0.8rem;
  font-size: 0.8125rem;
  white-space: nowrap;
}

.segmented button[aria-checked="true"] {
  background: var(--color-primary);
  color: #fff;
  font-weight: 500;
}

/* The rule reads as a sentence, so controls sit inline with the words around them. */
.phrase {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.9rem;
  color: var(--color-default-700);
}

.phrase .input--n {
  width: 4.5rem;
  text-align: center;
}

.hint {
  margin: 0.7rem 0 0;
  font-size: 0.78rem;
  color: var(--color-default-400);
}

/* ---------- Chips ---------- */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.chips + .chips { margin-top: 0.6rem; }

.chips--grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(2.75rem, 1fr));
}

.chip {
  border: 1px solid var(--color-default-300);
  background: transparent;
  border-radius: 0.375rem;
  padding: 0.35rem 0.6rem;
  cursor: pointer;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8rem;
  color: var(--color-default-500);
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.chip:hover {
  border-color: var(--color-primary);
  color: var(--color-default-800);
}

.chip[aria-pressed="true"] {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}

.chip--wide { font-family: inherit; }

/* ---------- Calendar ribbon ---------- */

.ribbon { margin-bottom: 1.5rem; }

.ribbon__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.ribbon__head .scheduler-subhead { margin-bottom: 0; }

.ribbon__nav {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

.ribbon__range {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.72rem;
  color: var(--color-default-400);
  min-width: 8.5rem;
  text-align: center;
}

.months {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: 0.9rem;
  margin-top: 0.75rem;
}

.month__name {
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-default-400);
  margin-bottom: 0.35rem;
}

.month__grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 1px;
}

.month__dow {
  font-size: 0.56rem;
  text-align: center;
  color: var(--color-default-300);
  padding-bottom: 0.15rem;
}

.day {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.64rem;
  text-align: center;
  padding: 0.18rem 0;
  border-radius: 0.2rem;
  color: var(--color-default-300);
}

.day--in { color: var(--color-default-500); }

.day--on {
  background: var(--color-primary);
  color: #fff;
  font-weight: 500;
}

/* A moved date keeps the fill and adds a rule underneath, so the two read as one family. */
.day--moved { box-shadow: inset 0 -2px 0 0 var(--color-secondary); }

.legend {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex-wrap: wrap;
  margin: 0.85rem 0 0;
  font-size: 0.7rem;
  color: var(--color-default-400);
}

.legend__swatch {
  display: inline-block;
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 0.2rem;
  background: var(--color-primary);
}

.legend__swatch--moved { box-shadow: inset 0 -2px 0 0 var(--color-secondary); }
.legend__swatch + .legend__swatch { margin-inline-start: 0.6rem; }

/* ---------- Date list ---------- */

.dates {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 18rem;
  overflow-y: auto;
  border: 1px solid var(--color-default-300);
  border-radius: 0.375rem;
}

.dates li {
  display: grid;
  grid-template-columns: 2.5rem 1fr auto;
  gap: 0.6rem;
  align-items: baseline;
  padding: 0.3rem 0.6rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.78rem;
  color: var(--color-default-700);
  border-bottom: 1px solid var(--color-default-200);
}

.dates li:last-child { border-bottom: 0; }
.dates li:nth-child(even) { background: var(--color-light); }

.dates .seq { color: var(--color-default-400); font-size: 0.7rem; }
.dates .dow { color: var(--color-default-400); font-size: 0.7rem; }

.dates .moved {
  color: var(--color-secondary);
  font-size: 0.66rem;
  font-family: inherit;
}

.dates--empty {
  display: block;
  padding: 1.25rem;
  color: var(--color-default-400);
  font-family: inherit;
  font-size: 0.82rem;
  text-align: center;
}

/* ---------- Saved frequencies drawer ---------- */

.library { position: fixed; inset: 0; z-index: 1050; }

.library__scrim {
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / 40%);
}

/* .card sets its own height and margin, so the drawer has to state both to fill the edge. */
.library__panel {
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  width: min(var(--panel-drawer), 100%);
  height: 100%;
  max-height: 100%;
  border-radius: 0;
  overflow-y: auto;
  margin: 0;
  display: flex;
  flex-direction: column;
  animation: scheduler-slide-in 0.18s ease;
}

.library__panel > .card-body { flex: 1 1 auto; }

@keyframes scheduler-slide-in {
  from { transform: translateX(1.5rem); opacity: 0; }
  to { transform: none; opacity: 1; }
}

.library__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.saved {
  width: 100%;
  text-align: start;
  background: transparent;
  border: 1px solid var(--color-default-300);
  border-radius: 0.375rem;
  padding: 0.7rem 0.8rem;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  transition: border-color 0.15s ease;
}

.saved:hover { border-color: var(--color-primary); }

.saved__name { font-weight: 500; color: var(--color-default-800); }

.saved__summary {
  font-size: 0.76rem;
  color: var(--color-default-400);
  text-wrap: pretty;
}

.saved__meta {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.68rem;
  color: var(--color-default-400);
  margin-top: 0.2rem;
}

.library__empty {
  color: var(--color-default-400);
  font-size: 0.82rem;
  text-align: center;
  padding: 2rem 0;
}

@media (prefers-reduced-motion: reduce) {
  .library__panel { animation: none; }
}

/* ---------- Slide-in drawer ---------- */

/*
  The same shape as the saved-frequencies drawer above, without its name. That one is called
  .library because it is the frequency library; this is the general one, and the two are kept apart
  rather than merged so renaming either cannot quietly restyle the other.
*/

/*
  ---------- How big the panels are ----------

  Every panel that comes in over the page — questions, progress, side panels, editors — in one
  place, and in proportion to the window rather than a fixed width. A fixed width chosen on a laptop
  is a narrow strip on a 27-inch screen, with long lines wrapped into a column and the page around it
  empty.

  Each is a share of the window between a floor and a ceiling. The floor is the size each one always
  had, so nothing gets smaller on a laptop than it was; the ceiling stops prose running into lines
  too long to read. Below the floor the panels already give way to the window — every use is
  min(…, 100%).
*/
:root {
  --panel-ask: clamp(26rem, 30vw, 44rem);
  --panel-ask-wide: clamp(36rem, 42vw, 60rem);
  --panel-ask-tall: clamp(46rem, 85vh, 64rem);
  --panel-run: clamp(32rem, 36vw, 50rem);
  --panel-run-tall: clamp(34rem, 70vh, 56rem);
  --panel-drawer: clamp(26rem, 24vw, 36rem);
  --panel-detail: clamp(44rem, 40vw, 60rem);
  --panel-editor: clamp(46rem, 48vw, 70rem);
}

.drawer { position: fixed; inset: 0; z-index: 1050; }
.drawer[hidden] { display: none; }

.drawer__scrim {
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / 40%);
}

/* .card sets its own height and margin, so the drawer has to state both to fill the edge. */
.drawer__panel {
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  width: min(var(--panel-drawer), 100%);
  height: 100%;
  max-height: 100%;
  border-radius: 0;
  overflow-y: auto;
  margin: 0;
  display: flex;
  flex-direction: column;
  animation: scheduler-slide-in 0.18s ease;
}

.drawer__panel > .card-body { flex: 1 1 auto; }

@media (prefers-reduced-motion: reduce) {
  .drawer__panel { animation: none; }
}

/*
  ---------- The filter drawer, docked beside the page ----------

  The same panel, stopped being an overlay. Three things change and they are all the same change:
  it is no longer in the way. The scrim goes, because there is nothing to dim; the drawer's own box
  shrinks to the width of the panel, so the rest of the viewport takes its own clicks again rather
  than passing them to a full-screen sheet with nothing drawn on it; and the page is indented by
  that width, so the grid ends where the panel begins instead of underneath it.

  It starts below the topbar rather than at the top of the screen. Full height would put a column of
  filters over the account menu and the theme switch, which belong to the whole application rather
  than to this page and should not be reachable only by unpinning.

  Only where there is room. Below this width the panel is most of the screen and a pinned one would
  be an overlay with the dimming turned off — see filter-pin.js, which leaves it an overlay here.
*/
@media (min-width: 64rem) {
  .drawer--pinned {
    inset-block-start: var(--topbar-height, 65px);
    inset-inline-start: auto;
    width: min(var(--panel-drawer), 100%);

    /*
      Under the panels, over the page.

      Pinned, this is part of the page rather than something on top of it — so anything that opens
      over the page opens over this too. A control's detail panel, the action editor and the
      assistant all come in from the same edge and would otherwise have to share it with a column of
      filters they were opened from. Above the expanded grid at 1040, which is not a panel: it is
      the page taking the whole window, and the filters belong beside it.
    */
    z-index: 1045;
  }

  .drawer--pinned .drawer__scrim { display: none; }

  /*
    The theme settings come in from the same edge, and belong to the whole application rather than
    this page — so they open over a docked filter column, not underneath it where nobody can reach
    them. The theme's own z-80 was written before anything docked there.
  */
  #theme-customization { z-index: 1062; }
  #theme-customization-backdrop { z-index: 1061; }

  .drawer--pinned .drawer__panel {
    animation: none;
    border-inline-start: 1px solid var(--color-default-200);
  }

  /* What the page gives up. The grids inside it are already told to fit whatever they are given. */
  body.has-pinned-filters .page-content { padding-inline-end: min(var(--panel-drawer), 100%); }

  /* And the same again for a grid that has taken the window, which is fixed and so takes no padding
     from the page it left behind. */
  body.has-pinned-filters .card--full { inset-inline-end: min(var(--panel-drawer), 100%); }
}

/*
  The Filters button while the docked panel is showing.

  It is a toggle now, so it says which way it is: pressed in, the way a switch that is on looks. Only
  the border and background — the filtering tone, when something is set, stays the text colour.
*/
.btn#open-filters.is-open {
  border-color: color-mix(in oklab, var(--color-primary) 55%, transparent);
  box-shadow: inset 0 1px 2px color-mix(in oklab, var(--color-default-900) 15%, transparent);
}

.btn#open-filters.is-open:not(.is-filtering) {
  background: color-mix(in oklab, var(--color-primary) 10%, transparent);
  color: var(--color-primary);
}

/* ---------- One control, read-only ---------- */

/*
  Wide enough for a label column and a value column side by side. The whole point of this panel is
  that fifty fields can be scanned at once, and stacking each label over its value would make it
  twice as long to scroll for no gain in legibility.
*/
.detail { width: min(var(--panel-detail), 100%); }

/*
  The fields scroll; which control they belong to does not. Fifty of them is a couple of thousand
  pixels, and a reader who has scrolled to the lifecycle dates should not have to scroll back to
  check whose they are — or to reach the buttons.
*/
.detail { overflow: hidden; }
.detail > .card-header,
.detail > .card-footer { flex: none; }
.detail > .card-body { overflow-y: auto; }

/*
  The strip between the header and the panes keeps its height, and the pane that is showing takes
  what is left. Without this the strip is an ordinary flex item and a long control squeezes it,
  which on a short window means the tabs thin out and then disappear behind their own panes.
*/
.detail > .control-tabs { flex: none; }
.detail > .card-body[hidden] { display: none; }

/* ---- How a control is being performed ---- */

/*
  The figures first, because the question somebody opening this tab has is whether the control is
  being done, and a list of ten dates does not answer it.
*/
.attestation-figures {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.25rem;
  margin-bottom: 0.9rem;
}

.attestation-figure {
  display: flex;
  align-items: baseline;
  gap: 0.3rem;
  font-size: 0.72rem;
  color: var(--color-default-500);
}

/* The number carries the colour; the word after it stays quiet. */
.attestation-figure strong {
  font-size: 1rem;
  font-weight: 600;
  color: var(--tone, var(--color-default-800));
}

.attestation-figure[data-tone='go'] { --tone: var(--color-status-passed); }
.attestation-figure[data-tone='stop'] { --tone: var(--color-status-failed); }
.attestation-figure[data-tone='wait'] { --tone: var(--color-warning); }
.attestation-figure[data-tone='new'] { --tone: var(--color-status-pending); }
.attestation-figure[data-tone='late'] { --tone: var(--color-status-overdue); }
.attestation-figure[data-tone='idle'] { --tone: var(--color-default-400); }

/* The dates underneath, for when the answer to the figures above is no. */
.attestation-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.attestation-list__row {
  display: grid;
  grid-template-columns: auto auto 1fr auto;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0;
  border-bottom: 1px solid var(--color-default-200);
  font-size: 0.75rem;
}

.attestation-list__row:last-child { border-bottom: 0; }

/*
  Here a pill hugs its text.

  .status-pill carries min-width: 11.5rem so that a column of them lines up in the grids, which is
  right there and wrong in a list six rows long inside a drawer — it made every status the width of
  the longest and left the name beside it nowhere to go.
*/
.attestation-list__row .status-pill {
  justify-self: start;
  min-width: 0;
}

.attestation-list__when {
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--color-default-600);
  white-space: nowrap;
}

.attestation-list__late {
  font-family: inherit;
  font-size: 0.65rem;
  color: var(--color-status-overdue);
}

.attestation-list__who {
  color: var(--color-default-500);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.attestation-list__files {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  color: var(--color-default-400);
}

/* ---- Expanded ---- */

/*
  The drawer is the right shape for glancing at one control beside the list, and the wrong one for
  reading fifty fields. Expanded, the panel takes the window and the sections flow into columns —
  most of a control on one screen instead of two thousand pixels of scrolling.
*/
.detail--full {
  inset: 0;
  width: 100%;
  animation: none;
}

/*
  Columns rather than a wider grid. Widening the grid would put four columns of label and value on
  one row and break the banding, which runs per pair; flowing whole sections keeps every section
  exactly as it reads in the drawer and only changes where they sit.

  On .detail__flow and not on the scrolling box around it: a multi-column box with a height forced
  on it paginates sideways rather than scrolling down, which lays half the control off the right of
  the screen. Inside the scroller, free to find its own height, it balances instead.
*/
.detail--full .detail__flow {
  column-width: 26rem;
  column-gap: 2.5rem;
}

/*
  In one column the first section does without the rule above it, because there is nothing above it
  to be ruled off from. In three it is only first in its own column, and whatever heads the other
  two starts level with it — so it takes the rule back and the three columns begin as one line.

  Padding and not margin, because a margin at the top of a column is dropped and padding is not:
  giving this one the margin the others appear to have had would sit it 22px below them.
*/
.detail--full .detail__section:first-child {
  margin-top: 0;
  padding-top: 1.1rem;
  border-top: 1px solid var(--color-default-200);
}

/* A heading has to keep the rows it belongs to, and none of these sections is a column tall. */
.detail__section {
  break-inside: avoid;
  page-break-inside: avoid;
}


.detail__ref {
  margin: 0 0 0.15rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.75rem;
  color: var(--color-default-400);
}

/*
  The rule and the space above a section belong to the section, not to its heading. On the heading
  they were a top margin on a first child, which collapses through its parent and made where a
  section actually started depend on what was above it — fine in one column, and quietly 20px out
  once the sections were flowing into three.
*/
.detail__section {
  margin-top: 1.4rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--color-default-200);
}

.detail__heading {
  margin: 0 0 0.6rem;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--color-default-900);
}

/* The rule between sections belongs between them, so the first one does without. */
.detail__section:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

/*
  A straight left edge of labels, so an eye running down it finds one field among fifty without
  reading any of the others. The label column is fixed rather than auto so that edge stays straight
  from section to section — an auto column would jog every time a section held a longer word.
*/
.detail__rows {
  display: grid;
  /* Wide enough that this deployment's longer names — "Control Quality Assurance Frequency" — mostly
     survive on one line. A wrapped label costs a whole extra band of height. */
  grid-template-columns: 14rem 1fr;
  margin: 0;
}

/*
  No column gap: the pairs are banded, and a gap would cut every band in two. The space between
  label and value is the label's own padding instead, which the band runs behind.
*/
.detail__k,
.detail__v {
  margin: 0;
  padding: 0.22rem 0;
  font-size: 0.8125rem;
  line-height: 1.4;
  min-width: 0;
}

.detail__k {
  padding-inline: 0.45rem 1.25rem;
  color: var(--color-default-500);
  text-wrap: pretty;
}

.detail__v {
  padding-inline-end: 0.45rem;
  color: var(--color-default-800);
  text-wrap: pretty;
  overflow-wrap: anywhere;
}

/*
  Every other pair banded. Fifty rows two columns wide is where an eye starts changing lanes on the
  way across, and a band is cheaper than the rules or the extra leading that would otherwise fix it.
*/
.detail__rows > .detail__k:nth-of-type(odd),
.detail__rows > .detail__v:nth-of-type(odd) {
  background: color-mix(in oklab, var(--color-default-500) 5%, transparent);
}

.detail__rows > .detail__k:nth-of-type(odd) {
  border-start-start-radius: 0.2rem;
  border-end-start-radius: 0.2rem;
}

.detail__rows > .detail__v:nth-of-type(odd) {
  border-start-end-radius: 0.2rem;
  border-end-end-radius: 0.2rem;
}

/* Not set, and saying so. A blank cell reads as a panel that forgot rather than a field nobody filled. */
.detail__v--empty { color: var(--color-default-300); }

/* All prose keeps of its own is the line breaks somebody typed. */
.detail__v--prose { white-space: pre-wrap; }

/* On a narrow screen the two columns cannot both survive, so the label goes above its value. */
@media (max-width: 36rem) {
  .detail__rows { grid-template-columns: 1fr; }

  .detail__k {
    padding: 0.35rem 0.45rem 0;
    color: var(--color-default-400);
    font-size: 0.75rem;
  }

  .detail__v { padding: 0 0.45rem 0.35rem; }

  .detail__rows > .detail__k:nth-of-type(odd) {
    border-radius: 0.2rem 0.2rem 0 0;
  }

  .detail__rows > .detail__v:nth-of-type(odd) {
    border-radius: 0 0 0.2rem 0.2rem;
  }
}

/* ---- Several named things ---- */

.detail__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
}

.detail__chip {
  padding: 0.05rem 0.45rem;
  border: 1px solid var(--color-default-200);
  border-radius: 999px;
  background: var(--color-default-100);
  font-size: 0.75rem;
  color: var(--color-default-700);
}

/* ---- The quality-assurance response ---- */

/*
  The criteria the control is being held against. A sunken box rather than a paragraph, because it
  is the thing being judged against rather than guidance about the form — and it is read beside a
  numbered list telling somebody what to do with it, which would otherwise run together.

  A minimum height so an empty one still reads as a box that happens to be empty, which is what it
  is: a control whose evaluation criteria nobody has written yet.
*/
.detail__prose {
  min-height: 7rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--color-default-200);
  border-radius: 0.35rem;
  background: var(--color-default-50, var(--color-default-100));
  font-size: 0.875rem;
  color: var(--color-default-700);
}

/* What the assessor is being asked to do, in the order they are asked to do it. */
.detail__steps-list {
  margin: 0;
  padding-left: 1.15rem;
  list-style: decimal;
  color: var(--color-default-600);
}

.detail__steps-list li + li { margin-top: 0.55rem; }

/*
  The three ratings, inside the step that asks for one.

  A column rather than a sentence. A status pill is an inline-flex box with its own padding, so it
  stands taller than the line it sits in — three of them run into a paragraph overlap the steps
  above and below, and the reader loses the pills and the words together.

  align-items keeps each pill the width of its own text; without it they stretch to the widest and
  read as three buttons of equal weight, which is not what a list of alternatives is.
*/
.rating-list {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
  margin: 0.5rem 0 0;
  padding-left: 0;
  list-style: none;
}

/*
  One attestation inside a review's period.

  Stacked cards rather than table rows, because the part that matters is the operator's comment and
  a table would truncate it. Somebody spot checking a quarter is reading rather than scanning.
*/
.detail__card {
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--color-default-200);
  border-radius: 0.35rem;
  background: var(--color-default-50, transparent);
}

.detail__card + .detail__card { margin-top: 0.5rem; }

/* ---- What has happened to it ---- */

/*
  The last few entries only. The whole history is a tab of its own in the editor, and a panel that
  tried to hold all of it would bury the fields above it.
*/
.detail__history {
  list-style: none;
  margin: 0;
  padding: 0;
}

.detail__event {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.15rem 0.6rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--color-default-200);
  font-size: 0.8rem;
}

.detail__event:last-child { border-bottom: 0; }

.detail__event-what { font-weight: 500; color: var(--color-default-800); }
.detail__event-when { color: var(--color-default-400); font-size: 0.75rem; text-align: end; }

.detail__event-who {
  grid-column: 1 / -1;
  color: var(--color-default-500);
  font-size: 0.75rem;
}

.detail__event-said {
  grid-column: 1 / -1;
  margin-top: 0.15rem;
  color: var(--color-default-700);
  text-wrap: pretty;
}

.detail__steps {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}

/* ---------- A grid that scrolls inside the page rather than moving it ---------- */

/*
  The page already fills the viewport: .page-content is a flex column of at least the height under
  the topbar, with the footer as its last child. What was missing was a way for the table to give up
  height rather than push the footer down — so the chain below grows every ancestor and lets each of
  them shrink, which is what min-height: 0 is for on a flex child. The table wrapper then takes the
  space that is left and scrolls what will not fit.

  Only below a certain height is this a bad trade: on a short viewport a bounded table is a couple of
  rows and a scrollbar, which is worse than an honest page that scrolls. So the whole arrangement is
  behind a media query and the page falls back to ordinary flow.
*/

@media (min-height: 34rem) {
  /*
    A ceiling, not just a floor. .page-content already asks for at least the height under the
    topbar; without a maximum it simply grows past it and takes the footer with it, and no amount
    of min-height: 0 below will make a child shrink inside a parent that is free to expand.
  */
  .library-page {
    height: calc(100dvh - var(--topbar-height, 65px));
    overflow: hidden;
  }

  .library-page > main,
  .library-page > main > .container-fluid,
  .library-page > main > .container-fluid > .card,
  .library-page .card > .card-body {
    display: flex;
    flex-direction: column;
    min-height: 0;
    flex: 1 1 auto;
  }

  /* The one part that scrolls. Everything else in the card keeps its natural height. */
  .library-page .table-wrapper {
    flex: 1 1 auto;
    min-height: 8rem;
    overflow-y: auto;
  }

  /* The tiles, the bulk bar, the count, the pager and the footer keep their own height — only the
     rows give way. */
  .library-page #status-tiles,
  .library-page .bulk-bar,
  .library-page .grid-foot,
  .library-page > .footer {
    flex: none;
  }
}

/* ---------- A grid taking the whole window ---------- */

/*
  The card is lifted out of the page rather than the page being hidden around it. Hiding would mean
  naming the topbar, the sidenav, the breadcrumb and the footer — parts of a theme layout this page
  does not own and which a template update is free to rename. One fixed card owns only itself.

  The flex chain is restated here rather than left to the .library-page media query above, because
  that query is off on a short viewport and this is precisely where a short viewport most wants its
  rows to scroll inside the card.
*/
.card--full {
  position: fixed;
  inset: 0;
  z-index: 1040;      /* over the page, under the drawers at 1050 and the run panel at 1070 */
  margin: 0;
  border-radius: 0;

  /*
    Stated rather than left to inset: 0. The theme sizes .card to its content, and a height that is
    not auto makes the browser ignore the bottom edge — the card grew to the length of the rows and
    took the pager off the screen with it.
  */
  height: 100dvh;
  max-height: 100dvh;

  display: flex;
  flex-direction: column;
}

.card--full > .card-body {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1 1 auto;
}

/* The rows are the only part that gives way; the header, the bulk bar, the chips and the count
   all keep the height they need. */
.card--full .table-wrapper {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

.card--full > .card-header,
.card--full > .filter-summary,
.card--full .bulk-bar,
.card--full .grid-foot,
.card--full > .alert {
  flex: none;
}

/*
  A grid that scrolls inside a capped height rather than inside the page.

  The config grids get sticky headers the same way the library does, but they cannot get them the
  same way round. A header sticks inside the nearest scrollport, so something has to scroll — and
  .library-page makes that the whole page, on the assumption the grid is the only thing on it. Half
  these pages are not that: Teams, Hierarchies and Reference lists carry a second card beside the
  grid, and Frequencies a block of its own below. Handing the viewport to the grid there would set
  those fighting the table for height and leave every one of them short.

  So the cap is on the wrapper alone. The page scrolls as it always did, whatever is under the grid
  keeps its own height, and the rows scroll within a bound the header can hold onto.

  Behind the same media query as the library layout and for the same reason: on a short viewport a
  capped table is two rows and a scrollbar, which is worse than a page that honestly scrolls.
*/
@media (min-height: 34rem) {
  .table-wrapper--capped {
    max-height: min(62vh, 40rem);
    overflow-y: auto;
  }
}

/*
  Scrollbars that are actually there, and can be dragged.

  macOS defaults to overlay scrollbars that fade out when nothing is moving. On a page that is
  survivable; on a box inside a page it is not, and on a mouse with no wheel it is the difference
  between a table you can read and a table you cannot. These grids scroll inside themselves by
  design now, so theirs are always drawn and always draggable.

  Styling ::-webkit-scrollbar is what does it: giving the pseudo-element a size opts the element out
  of overlay scrollbars entirely and back to classic ones, which take their own strip of layout and
  stay put. The catch, and the reason this looked done and was not, is that Chrome drops the whole
  ::-webkit- family the moment scrollbar-width or scrollbar-color is set on the same element — the
  standard properties win and take overlay behaviour with them. So the standard pair is kept for
  Firefox alone, behind a query for browsers that have no ::-webkit-scrollbar to begin with.
*/
.library-page .table-wrapper::-webkit-scrollbar,
.table-wrapper--capped::-webkit-scrollbar {
  width: 0.75rem;
  height: 0.75rem;
}

.library-page .table-wrapper::-webkit-scrollbar-track,
.library-page .table-wrapper::-webkit-scrollbar-corner,
.table-wrapper--capped::-webkit-scrollbar-track,
.table-wrapper--capped::-webkit-scrollbar-corner {
  background: var(--color-light);
}

/* The border is the track showing through, which insets the thumb rather than shrinking the target:
   the bar is still three-quarters of a rem of grabbable width. */
.library-page .table-wrapper::-webkit-scrollbar-thumb,
.table-wrapper--capped::-webkit-scrollbar-thumb {
  border-radius: 999px;
  border: 3px solid var(--color-light);
  background: var(--color-default-400);
}

.library-page .table-wrapper::-webkit-scrollbar-thumb:hover,
.table-wrapper--capped::-webkit-scrollbar-thumb:hover {
  background: var(--color-default-500);
}

@supports not selector(::-webkit-scrollbar) {
  .library-page .table-wrapper,
  .table-wrapper--capped {
    scrollbar-width: thin;
    scrollbar-color: var(--color-default-400) var(--color-light);
  }
}

/*
  Headers that stay while the rows move.

  They need an opaque background of their own: sticky cells sit over the rows, and the header's
  translucent tint would let them read through it. This is that tint resolved against the card, so
  the header looks the same as it did while behaving differently.

  Two selectors, one rule, because the two layouts differ only in what does the scrolling: the
  library hands its wrapper the height left in the page, a config grid caps its own.
*/
.library-page thead th,
.table-wrapper--capped thead th {
  position: sticky;
  top: 0;

  /* Above the row buttons, which the theme gives position: relative and z-index: 10 — without this
     they scroll over the header rather than under it. */
  z-index: 20;
  background: color-mix(in oklab, var(--color-light) 25%, var(--color-card));

  /*
    The line under the header, drawn as an inset shadow rather than a pseudo-element. A sortable
    header already spends its ::after on the caret that says which way the column is ordered, and
    an element has only one — taking it for a border left the caret as a bar under the text instead
    of a triangle beside it.
  */
  box-shadow: inset 0 -1px 0 0 var(--color-default-200);
}

/* ---------- The actions column, held against the right edge ---------- */

/*
  Twelve columns of nowrap text scrolls sideways on anything short of a desk monitor, and the actions
  are the last of them — so the column somebody scrolled across to reach was the column they then had
  to scroll back from. Pinned, it is where it always was and the rest of the row moves under it.

  The same trick the header uses, turned ninety degrees: sticky against the scrolling wrapper, with a
  background of its own. A sticky cell sits over what is passing beneath it, so it cannot be
  transparent and it cannot take a translucent tint — every state a row can be in has to be resolved
  against the card here rather than layered over it. That is what the three rules below are: the
  ordinary row, the hovered row, and the ticked row, each stated once more in opaque form.

  :not([colspan]) leaves the empty-state row alone. "No controls match these filters" is one cell
  spanning the table, which is last child and first child at once, and pinning a cell as wide as the
  table it is pinned inside is a way to make a message slide about for no reason.
*/
.table--pin-actions th:last-child:not([colspan]),
.table--pin-actions td:last-child:not([colspan]) {
  position: sticky;
  right: 0;

  /* Over the row buttons, which the theme gives position: relative and z-index: 10, and under the
     header at 20 — so a pinned cell passes beneath the header rather than through it. */
  z-index: 15;
  background: var(--color-card);

  /*
    A hairline down the leading edge and a soft shadow cast back over the rows, so the column reads
    as sitting above the table rather than as part of it. It is worn all the time rather than only
    while there is something behind it: macOS hides its scrollbars until something is already moving,
    so without a standing edge here nothing on the page says the table continues past it until
    somebody has already found out. Unscrolled it reads as a column rule, which is no loss.

    Held in a custom property because the corner cell below carries the header's underline as well
    and would otherwise have to restate both of these to add a third.
  */
  --pin-edge: inset 1px 0 0 0 var(--color-default-200), -10px 0 10px -8px rgb(0 0 0 / 22%);

  box-shadow: var(--pin-edge);
}

/*
  The corner cell is sticky both ways at once, so it has to clear the header and the column alike —
  and carry both of their lines, because the header's underline is an inset shadow that the pinned
  column's own shadow would otherwise replace.

  It repeats the :not([colspan]) above so that it outweighs it. Without it this selector is the
  lighter of the two, the corner keeps the column's z-index of 15 rather than taking 25, and the body
  cells — same z-index, later in the document — paint straight over the header as they scroll under
  it. Which is a heading that vanishes the moment anybody scrolls.
*/
.table--pin-actions thead th:last-child:not([colspan]) {
  z-index: 25;
  background: color-mix(in oklab, var(--color-light) 25%, var(--color-card));
  box-shadow: var(--pin-edge), inset 0 -1px 0 0 var(--color-default-200);
}

/* bg-light/45 on the row, resolved rather than laid over. */
.table--pin-actions.table-hover tbody tr:hover td:last-child:not([colspan]) {
  background: color-mix(in oklab, var(--color-light) 45%, var(--color-card));
}

/* bg-warning/10 on a ticked row, likewise — otherwise the one cell that carries the row's actions is
   the one cell that does not look selected. */
.table--pin-actions:has(.form-checkbox:checked) tr:has(.form-checkbox:checked) td:last-child:not([colspan]) {
  background: color-mix(in oklab, var(--color-warning) 10%, var(--color-card));
}

/* ---------- Icon buttons that say what they do ---------- */

/*
  A row of small icons is quick to scan and slow to read: a tick, a cross and an eye are obvious
  once you know them and a guess until then, and the tooltip that would settle it arrives a second
  later, in the corner, in the operating system's own type. So the button under the pointer opens
  out and says its word instead — immediately, in place, in the colour it already wears.

  The word is uncovered rather than typed out: the pill is laid out at its full size and then
  clipped away to nothing against the button's edge, and what animates is the clip retreating. A
  width could not do this job — the pill is out of the document's flow and so is sized by its own
  contents, which leaves a percentage or an fr with nothing to be a fraction of — and a max-width
  guessed at in the stylesheet would run the animation at a different speed for every word.

  Nothing about this changes the layout, and that is the whole design rather than an economy. The
  first version of it widened the button itself, which pushed its neighbours along the row — and a
  button that moves the things around it while the pointer is resting between two of them moves one
  of them under the pointer, which opens that one, which moves the first back. Approaching the row
  slowly set it flickering between two buttons. So the word is painted outside the button instead:
  the square keeps its size and its place, and what grows is an absolutely positioned extension to
  the left of it, over whatever is there. Nothing in the row moves, so nothing can flap.

  It follows that the extension takes no clicks — pointer-events: none — so the button's target
  stays the square it always was and the pointer sees through the word to the button underneath.
  Sliding left off one button lands on the next one, and that one opens in its turn.

  Keyboard users get the same on focus, which is also what makes this reachable without a pointer.
*/

/*
  The two halves have to meet as one pill, so both are painted from the same pair of colours.
  Opaque, because the extension covers a neighbouring button and a translucent tint would let it
  ghost through — which is why the tone is mixed towards the card here rather than towards
  transparent as .btn-tone does it.
*/
.btn-icon:has(.btn-icon__label) {
  --pill-face: var(--color-light);
  --pill-edge: var(--color-default-300);
}

.btn-tone:has(.btn-icon__label) {
  --pill-face: color-mix(in oklab, var(--tone) 22%, var(--color-card));
  --pill-edge: color-mix(in oklab, var(--tone) 35%, var(--color-card));
}

.btn-icon__label {
  position: absolute;

  /* Against the button's left edge, and the full height of its border box — the inset of a pixel
     top and bottom is the button's own border, which this has to line up with rather than sit
     inside. */
  right: 100%;
  top: -1px;
  bottom: -1px;

  display: flex;
  align-items: center;
  padding-inline: calc(var(--spacing) * 2) calc(var(--spacing) * 1.5);
  pointer-events: none;

  background: var(--pill-face);
  border: 1px solid var(--pill-edge);
  border-right: 0;
  border-radius: var(--radius) 0 0 var(--radius);
  font-size: var(--text-xs);
  white-space: nowrap;

  /* Clipped away entirely against the button's edge, which is where it grows back from. */
  clip-path: inset(0 0 0 100%);
  opacity: 0;
  transition: clip-path .18s ease, opacity .12s ease;
}

/*
  The open state. The button takes the same face and edge so the join is one colour, loses the
  corners on the side the word joins, and paints its own left border in the face colour — which is
  the seam between the two halves, made invisible rather than removed, so neither half moves by the
  pixel that removing it would cost.
*/
.btn-icon:has(.btn-icon__label):hover,
.btn-icon:has(.btn-icon__label):focus-visible {
  background: var(--pill-face);
  border-color: var(--pill-edge);
  border-left-color: var(--pill-face);
  border-start-start-radius: 0;
  border-end-start-radius: 0;

  /* Over the neighbour it is about to lean across. */
  z-index: 11;
}

.btn-icon:has(.btn-icon__label):hover .btn-icon__label,
.btn-icon:has(.btn-icon__label):focus-visible .btn-icon__label {
  clip-path: inset(0 0 0 0);
  opacity: 1;
}

/*
  Somebody who has asked for less movement still gets the word — it is information, not decoration —
  but gets it at once rather than sliding out.
*/
@media (prefers-reduced-motion: reduce) {
  .btn-icon__label { transition: none; }
}

/* ---------- The actions column, held against the left edge instead ---------- */

/*
  The same column, the other way round, for a reader who would rather have the buttons where the row
  starts than where it ends. The script swaps one class for the other, never both at once, so none of
  the :last-child rules above are fighting these — the last column is an ordinary column again.

  Two cells are held rather than one: the actions, and the tick box that leads them. Pinning only the
  actions would leave the tick box to slide underneath, which puts a checkbox and the buttons that
  act on it in two different rows as far as the eye is concerned. How far in the actions sit is
  measured rather than assumed — the tick box is sized by the theme — and handed over as
  --pin-start-offset.
*/
.table--pin-start th.grid-col--pin-lead:not([colspan]),
.table--pin-start td.grid-col--pin-lead:not([colspan]),
.table--pin-start th.grid-col--pin-start:not([colspan]),
.table--pin-start td.grid-col--pin-start:not([colspan]) {
  position: sticky;
  z-index: 15;
  background: var(--color-card);
}

.table--pin-start .grid-col--pin-lead { left: 0; }

/* The edge and its shadow, mirrored: the rule is down the trailing edge and the shadow falls right,
   over the rows passing beneath. */
.table--pin-start .grid-col--pin-start {
  left: var(--pin-start-offset, 0px);
  --pin-edge: inset -1px 0 0 0 var(--color-default-200), 10px 0 10px -8px rgb(0 0 0 / 22%);

  box-shadow: var(--pin-edge);
}

/*
  Sticky both ways at once, so it clears the header as well as the column — and carries the header's
  underline along with its own edge.

  :not([colspan]) is carried down from the rule above rather than dropped, because without it these
  two are the less specific pair and lose: the pinned headers would take the ordinary header's
  z-index and be painted over by the columns sliding past, leaving a gap where the heading was.
*/
.table--pin-start thead th.grid-col--pin-lead:not([colspan]),
.table--pin-start thead th.grid-col--pin-start:not([colspan]) {
  z-index: 25;
  background: color-mix(in oklab, var(--color-light) 25%, var(--color-card));
}

.table--pin-start thead th.grid-col--pin-start:not([colspan]) {
  box-shadow: var(--pin-edge), inset 0 -1px 0 0 var(--color-default-200);
}

/* The hovered and the ticked row, resolved against the card rather than laid over it — the same
   three states the right-hand pin states, for the same reason. */
.table--pin-start.table-hover tbody tr:hover td.grid-col--pin-lead,
.table--pin-start.table-hover tbody tr:hover td.grid-col--pin-start {
  background: color-mix(in oklab, var(--color-light) 45%, var(--color-card));
}

.table--pin-start:has(.form-checkbox:checked) tr:has(.form-checkbox:checked) td.grid-col--pin-lead,
.table--pin-start:has(.form-checkbox:checked) tr:has(.form-checkbox:checked) td.grid-col--pin-start {
  background: color-mix(in oklab, var(--color-warning) 10%, var(--color-card));
}

/* ---------- Columns the reader arranges ---------- */

/*
  A header that can be dragged says so under the pointer, and only then: a grab hand standing on
  every heading all the time would read as "these do nothing else", when the first thing a heading
  does is sort.
*/
.grid-col--movable { cursor: grab; }
.grid-col--movable:active { cursor: grabbing; }

/* The column being carried, faded so the row underneath stays readable while it moves. */
.table th.is-dragging {
  opacity: .45;
  cursor: grabbing;
}

/*
  Where it would land, drawn as a rule down the edge it would land against — inside the cell rather
  than as a border, so nothing in the header shifts by a pixel while the reader is aiming at it.
*/
.table th.is-drop-target { box-shadow: inset 2px 0 0 0 var(--color-primary); }
.table th.is-drop-end { box-shadow: inset -2px 0 0 0 var(--color-primary); }

/* A column put away. Both ends of it go: the heading and every cell beneath. */
.grid-col--hidden { display: none; }

/*
  The picker. Fixed rather than absolute because it is placed against the button's position on
  screen, and the grid it belongs to scrolls inside its own wrapper.
*/
.column-picker {
  position: fixed;
  z-index: 1060;
  min-width: 15rem;
  max-height: min(24rem, 70vh);
  overflow-y: auto;
  padding: .75rem;
  border: 1px solid var(--color-default-200);
  border-radius: .5rem;
  background: var(--color-card);
  box-shadow: 0 12px 32px rgb(0 0 0 / 18%);
}

.column-picker__head {
  font-size: .75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--color-default-500);
}

.column-picker__hint {
  margin: .25rem 0 .5rem;
  font-size: .75rem;
  color: var(--color-default-400);
}

.column-picker__list {
  display: flex;
  flex-direction: column;
  gap: .125rem;
}

.column-picker__item {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .3rem .35rem;
  border-radius: .375rem;
  font-size: .875rem;
  cursor: pointer;
}

.column-picker__item:hover { background: color-mix(in oklab, var(--color-light) 45%, var(--color-card)); }

/* The last column showing cannot be put away — it reads as unavailable rather than as broken. */
.column-picker__item:has(input:disabled) {
  cursor: default;
  opacity: .55;
}

.column-picker__reset {
  margin-top: .5rem;
  width: 100%;
  padding: .35rem;
  border: 1px solid var(--color-default-200);
  border-radius: .375rem;
  background: none;
  font-size: .8125rem;
  color: var(--color-default-600);
  cursor: pointer;
}

.column-picker__reset:hover { background: var(--color-light); }

/* A second heading inside the panel, which needs air above it that the first one does not. */
.column-picker__head--second { margin-top: .75rem; }

/* Which end the actions sit at: two alternatives, so a segmented pair rather than two buttons. */
.column-picker__sides {
  display: flex;
  margin-top: .35rem;
}

.column-picker__side {
  flex: 1;
  padding: .3rem;
  border: 1px solid var(--color-default-200);
  background: none;
  font-size: .8125rem;
  color: var(--color-default-600);
  cursor: pointer;
}

.column-picker__side:first-child { border-radius: .375rem 0 0 .375rem; }
.column-picker__side:last-child { border-radius: 0 .375rem .375rem 0; border-left-width: 0; }
.column-picker__side:hover { background: var(--color-light); }

.column-picker__side[aria-pressed="true"] {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}

/* ---------- The keys a grid answers, written down ---------- */

/*
  Same card as the column picker, and placed the same way — fixed, against the button that opens it —
  because it is the same kind of thing: a small list belonging to a button in the grid's header.
*/
.key-legend {
  position: fixed;
  z-index: 1060;
  min-width: 19rem;
  max-width: min(26rem, calc(100vw - 1rem));
  max-height: min(28rem, 70vh);
  overflow-y: auto;
  padding: .75rem;
  border: 1px solid var(--color-default-200);
  border-radius: .5rem;
  background: var(--color-card);
  box-shadow: 0 12px 32px rgb(0 0 0 / 18%);
}

.key-legend__head {
  font-size: .75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--color-default-500);
}

.key-legend__head--second { margin-top: .85rem; }

.key-legend__hint {
  margin: .25rem 0 .1rem;
  font-size: .75rem;
  color: var(--color-default-400);
  text-wrap: pretty;
}

/*
  Two columns: the keys, and what they do. The keys column is sized to its widest row rather than
  guessed at, so the words all start at the same place however long "Ctrl + Shift + P" renders.
*/
.key-legend__list {
  display: grid;
  grid-template-columns: max-content 1fr;
  align-items: baseline;
  gap: .35rem .6rem;
  margin: .45rem 0 0;
}

.key-legend__keys {
  display: flex;
  align-items: center;
  gap: .2rem;
  margin: 0;
  white-space: nowrap;
}

.key-legend__said {
  margin: 0;
  font-size: .8125rem;
  color: var(--color-default-700);
  text-wrap: pretty;
}

/* A key as a key: the shape of the thing being pressed, in the type a keyboard is labelled in. */
.key-legend__key {
  display: inline-block;
  min-width: 1.4rem;
  padding: .1rem .3rem;
  border: 1px solid var(--color-default-200);
  border-bottom-width: 2px;
  border-radius: .3rem;
  background: color-mix(in oklab, var(--color-light) 55%, var(--color-card));
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .7rem;
  line-height: 1.35;
  text-align: center;
  color: var(--color-default-700);
}

/* ---------- A step running over several controls ---------- */

/*
  The ledger is the one loud thing here; everything around it is deliberately quiet. The bar is a
  hairline rather than a chunky rounded gauge, the panel has no shadow theatre, and the only colour
  is the tick and the cross — which is the only place colour carries meaning.
*/

.run {
  position: fixed;
  inset: 0;
  z-index: 1070;         /* over the drawer and over a combo list left open behind it */
  display: grid;
  place-items: center;
  padding: 1.5rem;
}

.run__scrim {
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / 45%);
}

.run__panel {
  position: relative;
  width: min(var(--panel-run), 100%);

  /*
    Against the viewport, not against the grid area.

    `min(34rem, 100%)` read as though it capped the panel at whichever was smaller, and it does not:
    a percentage max-height resolves against this grid's area, which is not definite enough for it,
    so the 34rem branch always won. On a window shorter than 34rem plus the padding the panel simply
    grew past both edges — centred, so it hung off the top and took its Stop button off the bottom.

    A viewport unit is always definite. dvh rather than vh so a phone's collapsing toolbar cannot
    put the foot of the panel under it.
  */
  max-height: min(var(--panel-run-tall), calc(100vh - 3rem));
  max-height: min(var(--panel-run-tall), calc(100dvh - 3rem));

  display: flex;
  flex-direction: column;
  padding: 1.5rem;
  border-radius: 0.5rem;
  background: var(--color-card);
  animation: run-appear 0.16s ease-out;
}

@keyframes run-appear {
  from { opacity: 0; transform: translateY(0.5rem); }
  to { opacity: 1; transform: none; }
}

.run__title {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-default-900);
}

/* The running count, in the same mono the reference numbers use — it is a figure, not prose. */
.run__tally {
  margin: 0.35rem 0 0.9rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.72rem;
  color: var(--color-default-400);
  min-height: 1rem;
}

.run__bar {
  height: 2px;
  border-radius: 2px;
  background: var(--color-default-200);
  overflow: hidden;
}

.run__bar-fill {
  height: 100%;
  width: 0;
  background: var(--color-primary);
  transition: width 0.25s ease-out;
}

/* Settled rather than triumphant: the run is over, whatever the outcome was. */
.run__bar-fill--settled { background: var(--color-default-400); }

/*
  Something is happening, and that is all this claims.

  One request that answers when it is done has no percentage to report, and a bar creeping to 90%
  on a timer would be inventing one. A stripe travelling across says the same true thing — it is
  still going — without pretending to know how far along it is.
*/
.run__bar-fill--waiting {
  width: 32%;
  background: var(--color-primary);
  animation: run-waiting 1.15s ease-in-out infinite;
}

@keyframes run-waiting {
  from { transform: translateX(-110%); }
  to { transform: translateX(320%); }
}

/* ---- The ledger ---- */

/*
  The ledger takes its height at the start rather than earning it line by line.

  Sized by its content, the panel grew as the run went on — a hundred and fifty pixels over a batch
  of forty — and because the panel is centred it grew in both directions at once. The Stop button
  someone was reaching for walked down the screen away from them. One size for the whole run is
  worth more than a snug fit around three lines.

  It still shrinks — min-height: 0, and the panel's own cap above it — so a short window gets a
  short ledger rather than a panel hanging off the edge.
*/
.run__ledger {
  list-style: none;
  margin: 1rem 0 0;
  padding: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  flex: 1 1 auto;
  height: 16rem;
  min-height: 0;
}

/*
  A ledger with three lines in it, rather than the fixed sixteen rems a batch gets.

  The height a batch uses is there so the panel does not grow line by line while somebody reads it.
  A single operation writes its result once, at the end, so there is nothing to steady — and a
  mostly empty box under a three-line answer just makes the dialog look like it lost something.
*/
.run__ledger--short {
  height: auto;
  min-height: 2.5rem;
  max-height: 16rem;
}

.run__line {
  display: grid;

  /*
    The middle column is what the run calls each item, and the caller decides how long that is: a
    control library batch says "10836", an attestation batch says "10836 · 2026-08-04". Fixed at
    4.5rem the second broke across three lines and made every row three times as tall, which is most
    of how the ledger got long enough to be a problem.
  */
  grid-template-columns: 1rem minmax(4.5rem, max-content) 1fr;
  gap: 0.5rem;
  align-items: baseline;
  padding: 0.3rem 0;
  border-bottom: 1px solid var(--color-default-200);
  font-size: 0.8rem;
  animation: run-line 0.15s ease-out;
}

.run__line:last-child { border-bottom: 0; }

@keyframes run-line {
  from { opacity: 0; transform: translateY(-0.2rem); }
  to { opacity: 1; transform: none; }
}

.run__mark { font-size: 0.9rem; }
.run__line--done .run__mark { color: var(--color-success); }
.run__line--refused .run__mark { color: var(--color-danger); }

/* The reference number, in the mono it is printed in everywhere else. */
.run__ref {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.75rem;
  color: var(--color-default-500);

  /* It is one label. Wrapped, "10836 · 2026-08-04" reads as three separate figures. */
  white-space: nowrap;
}

.run__said {
  color: var(--color-default-700);
  text-wrap: pretty;
}

.run__line--refused .run__said { color: var(--color-danger); }

/* ================================================================================
   Notes, and the bar that writes them

   What is stored is plain text carrying a light convention — see rich-text.js. These rules are the
   two ends of it: the bar over the box, and how the convention reads once it is on a screen.
   ================================================================================ */

.rich-bar {
  display: flex;
  align-items: center;
  gap: 0.15rem;
  margin-bottom: -1px;
  padding: 0.25rem 0.3rem;
  border: 1px solid var(--color-default-300);
  border-radius: 0.375rem 0.375rem 0 0;
  background: var(--color-default-100, rgb(0 0 0 / 3%));
}

/* Square with the bar above it, so the two read as one control rather than two stacked ones. */
.rich-input {
  border-top-left-radius: 0;
  border-top-right-radius: 0;
  resize: vertical;
  min-height: 6rem;
  font-family: inherit;
}

.rich-bar__tool {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border: 0;
  border-radius: 0.25rem;
  background: transparent;
  color: var(--color-default-600);
  cursor: pointer;
}

.rich-bar__tool:hover { background: var(--color-default-200, rgb(0 0 0 / 6%)); }
.rich-bar__tool:active { color: var(--color-primary); }

.rich-bar__tool:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: -2px;
}

/* Says what the bar is doing, because a bar that writes asterisks needs to admit it. */
.rich-bar__note {
  margin-inline-start: auto;
  padding-inline-end: 0.2rem;
  font-size: 0.65rem;
  color: var(--color-default-400);
}

@media (max-width: 30rem) {
  .rich-bar__note { display: none; }
}

/* ---- A note, rendered ---- */

.rich > :first-child { margin-top: 0; }
.rich > :last-child { margin-bottom: 0; }

.rich__p {
  margin: 0 0 0.5rem;
  text-wrap: pretty;
}

/*
  Markers stated rather than inherited. The host's reset takes list-style off every ul and ol, which
  is right for the menus and the grids that make up most of this application and wrong for the one
  place somebody has actually written a list — without it the bullets they typed come out as
  indented lines with nothing in front of them.
*/
ul.rich__list {
  margin: 0 0 0.5rem;
  padding-inline-start: 1.2rem;
  list-style: disc outside;
}

ol.rich__list {
  margin: 0 0 0.5rem;
  padding-inline-start: 1.4rem;
  list-style: decimal outside;
}

.rich__list li {
  margin: 0.1rem 0;
  text-wrap: pretty;
}

.rich strong { font-weight: 600; }

.rich__code {
  padding: 0.05rem 0.25rem;
  border-radius: 0.2rem;
  background: var(--color-default-200, rgb(0 0 0 / 6%));
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.9em;
}

.rich__link {
  color: var(--color-primary);
  text-decoration: underline;
  overflow-wrap: anywhere;
}

/* In a history entry, where the note sits under a line of who and when. */
.rich--small {
  font-size: 0.72rem;
  line-height: 1.45;
}

/*
  The foot: what became of the batch, and the way out of it.

  The summary is on the left and grows downward; the button stays pinned to the bottom right where
  it has been all along, so it does not move when the reasons appear under the counts.
*/
.run__foot {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  padding-top: 1rem;
}

/* Copy and Close together at the end, so the summary keeps the whole of the left. */
.run__actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: none;
}

/* Wide enough that it does not jump when the label becomes "Copied". */
.run__copy {
  min-width: 6.5rem;
  justify-content: center;
}

.run__summary {
  min-width: 0;
  flex: 1 1 auto;
}

.run__counts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 0.75rem;
  font-size: 0.75rem;
  color: var(--color-default-500);
}

/* A dot in the tone of what it counts, so the two figures are told apart before they are read. */
.run__count {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  white-space: nowrap;
}

.run__count::before {
  content: '';
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 50%;
  background: currentcolor;
  opacity: 0.5;
}

.run__count--done { color: var(--color-success); }
.run__count--done::before { opacity: 1; }

.run__count--refused { color: var(--color-danger); }
.run__count--refused::before { opacity: 1; }

/* Why the refusals were refused, commonest first. */
.run__reasons {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
  font-size: 0.72rem;
  color: var(--color-default-500);
}

.run__reasons > li {
  display: grid;
  grid-template-columns: 1.75rem 1fr;
  gap: 0.4rem;
  padding: 0.1rem 0;
  text-wrap: pretty;
}

.run__reason-count {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--color-danger);
  text-align: right;
}

/* Qualified, or the two-column rule above it wins and puts this sentence in the count column. */
.run__reasons > .run__reasons-more {
  display: block;
  padding-top: 0.15rem;
  color: var(--color-default-400);
  font-style: italic;
}

@media (prefers-reduced-motion: reduce) {
  .run__panel,
  .run__line { animation: none; }
  .run__bar-fill { transition: none; }

  /* Still has to say something is happening, so it fills the track instead of travelling along it. */
  .run__bar-fill--waiting {
    width: 100%;
    animation: none;
    opacity: 0.45;
  }
}

/* ---- Asking a yes or no question ---- */

/*
  The same panel as the run, narrower and without the ledger, because it is the same interruption
  wearing the same clothes — see ask.js for why the browser's own confirm() is not used.
*/
.ask__panel { width: min(var(--panel-ask), 100%); }

/*
  The panel caps its own height and its body is what gives, so a question with a form in it scrolls
  its fields rather than growing past the window and taking the two buttons off the bottom with it.
  The run panel does not need this — its ledger already scrolls — which is why it is here.
*/
.ask__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* Wider, and taller, for the questions that carry a form: a two-column row does not fit 26rem. */
.ask__panel--wide {
  width: min(var(--panel-ask-wide), 100%);
  max-height: min(var(--panel-ask-tall), calc(100vh - 3rem));
  max-height: min(var(--panel-ask-tall), calc(100dvh - 3rem));
}

.ask__prompt {
  margin: 0.6rem 0 0;
  font-size: 0.875rem;
  color: var(--color-default-700);
  text-wrap: pretty;
}

.ask__hint {
  margin: 0.5rem 0 0;
  font-size: 0.8rem;
  color: var(--color-default-500);
  text-wrap: pretty;
}

.ask__form {
  margin-top: 1rem;
}

.ask__form .form-label {
  display: block;
  margin-bottom: 0.35rem;
}

.ask__form .form-input {
  width: 100%;
}

.ask__error {
  margin: 0.4rem 0 0;
  font-size: 0.8rem;
  color: var(--color-danger);
}

.ask__error[hidden] { display: none; }

/*
  What a look at the answer found before it was let through — the AI evidence check, for now.

  Below the form and above the buttons, because it is the last thing to read before pressing one of
  them. Amber rather than red: it is advice about the file, not a refusal of the answer.
*/
.ask__review { margin-top: 0.9rem; }
.ask__review[hidden] { display: none; }

/*
  The AI evidence check while it runs: a small status card with an indeterminate bar.

  Nothing in it rotates or scales. A spinning circle at the foot of the scrolling body pushed its
  corners past the edge on every turn, and the body's scrollbar came and went with it. The bar
  slides inside a clipped track and the icon only fades, so the card never changes the body's size.
*/
.ask__checking {
  contain: paint;
  padding: 0.75rem 0.9rem 0.85rem;
  border: 1px solid color-mix(in oklab, var(--color-primary) 22%, transparent);
  border-radius: 0.5rem;
  background: color-mix(in oklab, var(--color-primary) 5%, var(--color-card));
}

.ask__checking-head {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--color-default-800);
}

.ask__checking-icon {
  color: var(--color-primary);
  font-size: 1rem;
  animation: ask-pulse 1.6s ease-in-out infinite;
}

.ask__checking-time {
  margin-left: auto;
  font-size: 0.75rem;
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  color: var(--color-default-500);
}

.ask__checking-text {
  margin: 0.3rem 0 0.65rem;
  font-size: 0.78rem;
  color: var(--color-default-500);
}

.ask__progress {
  position: relative;
  height: 3px;
  overflow: hidden;
  border-radius: 999px;
  background: color-mix(in oklab, var(--color-primary) 14%, transparent);
}

.ask__progress::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 35%;
  border-radius: inherit;
  background: var(--color-primary);
  animation: ask-progress 1.5s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}

@keyframes ask-progress {
  from { transform: translateX(-100%); }
  to   { transform: translateX(290%); }
}

@keyframes ask-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.45; }
}

@media (prefers-reduced-motion: reduce) {
  .ask__checking-icon { animation: none; }
  .ask__progress::after { animation-duration: 3s; }
}

.evidence-warning {
  padding: 0.75rem 0.9rem;
  border: 1px solid color-mix(in oklab, var(--color-warning) 45%, transparent);
  border-radius: 0.5rem;
  background: color-mix(in oklab, var(--color-warning) 9%, var(--color-card));
  font-size: 0.8125rem;
  color: var(--color-default-700);
}

.evidence-warning p { margin: 0; text-wrap: pretty; }

.evidence-warning__head {
  display: flex;
  align-items: flex-start;
  gap: 0.65rem;
  padding-bottom: 0.65rem;
  margin-bottom: 0.15rem;
  border-bottom: 1px solid color-mix(in oklab, var(--color-warning) 30%, transparent);
}

.evidence-warning__badge {
  display: grid;
  place-items: center;
  flex: none;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 0.5rem;
  background: color-mix(in oklab, var(--color-warning) 18%, var(--color-card));
  color: color-mix(in oklab, var(--color-warning) 80%, var(--color-default-900));
  font-size: 1.05rem;
}

.evidence-warning__heading { flex: 1 1 auto; min-width: 0; }

.evidence-warning__title {
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.005em;
  color: var(--color-default-900);
}

.evidence-warning__text {
  margin-top: 0.15rem !important;
  font-size: 0.78rem;
  color: var(--color-default-500);
}

.evidence-warning__tag {
  margin-bottom: 0.1rem !important;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--color-warning) 70%, var(--color-default-900));
}

.evidence-warning__files {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin: 0.6rem 0;
  padding: 0;
  list-style: none;
}

.evidence-warning__file {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}

.evidence-warning__file strong {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.evidence-warning__why {
  margin-top: 0.25rem !important;
  color: var(--color-default-600);
}

.evidence-warning__ask { font-weight: 500; }

/*
  The answer sits on the right, after the way out. Reversed on a phone, where the thumb reaches the
  bottom of the panel before the top of it.
*/
.ask__foot {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  padding-top: 1.25rem;
}

@media (max-width: 30rem) {
  .ask__foot {
    flex-direction: column-reverse;
  }
}

/* ---------- Acting on several at once ---------- */

/*
  Sits under the rows and over the count, so what is selected and what can be done with it are read
  in the same glance as the rows they refer to.

  Under rather than over them because it is not always there: it arrives with the first row picked
  up and goes with the last one put down. Above the table that would move the rows down the page
  mid-gesture — and a row that moves between the two halves of a double-click is a row the second
  half misses. Below, the table keeps its top edge and only gives up height at the bottom.
*/
.bulk-bar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  padding: 0.6rem 1rem;
  border-top: 1px solid var(--color-default-200);
  background: color-mix(in oklab, var(--color-primary) 8%, transparent);
}

.bulk-bar[hidden] { display: none; }

.bulk-bar__count {
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--color-default-900);
  white-space: nowrap;
}

.bulk-bar__actions {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
  flex: 1 1 auto;
}

/* ---------- One palette for what a thing does ---------- */

/*
  The four answers, written down once.

  Passed, failed, pending and overdue are the four things this application ever says about a due
  date, and every surface that says one of them — a pill, a tile, a bar, a figure in a table, a
  series in a chart — reads its colour from here. Named --color-* so the chart helper in app.js,
  which looks up --color-<name>, can reach them by the same names the stylesheets use.

    passed   green, from the skin
    failed   red, from the skin
    pending  blue, written down
    overdue  purple, written down

  Passed and failed follow the skin because every skin's success is a green and its danger is a
  red — that much the palette can be trusted with. Pending and overdue cannot be: the skins put
  their secondary anywhere from a blue to a slate grey, and their purple close enough to their
  primary to disappear into it, and these two have to stay a blue and a purple on all twelve. So
  they are written down. The blue is oklch(0.58 0.16 264) and the purple oklch(0.50 0.20 300),
  which is where the two of them were chosen: thirty-six degrees of hue apart, and eight points of
  lightness apart as well, because hue alone does not separate a blue from a purple for a reader
  who cannot see the red in one of them.

  Written as hex rather than as the oklch they were picked in, because ApexCharts parses a colour
  itself before it fills anything with it, and anything that is not a hex or an rgba it silently
  replaces with #999999. The charts read these four through the theme() helper, so a grey chart is
  what oklch here would buy.
*/
:root {
  --color-status-passed: var(--color-success);
  --color-status-failed: var(--color-danger);
  --color-status-pending: #4874d8;
  --color-status-overdue: #773ac1;

  /*
    Not a status at all: the tile that counts the lot, and the way back to it.

    Its own yellow because the nearest thing the theme had was the warning amber, and that is already
    worn by Pending approval and Pending deactivation in the same row of tiles — three in one colour,
    two of which mean somebody has to do something and one of which means nothing at all. This one is
    deeper and further from orange, so the row reads as a total and two pendings rather than three of
    a kind.
  */
  --color-status-total: #e0a800;
}

/*
  Lifted for the dark card, which the light two do not need.

  Green and red come from the skin and the skin already re-picks them for dark. The blue and the
  purple do not, and the purple as chosen falls to 2.5:1 against #1e1f27 — a bar nobody can find.
  Both are raised by the same amount, so the eight points of lightness that tell them apart survive
  the switch and pending stays the lighter of the two on both themes.
*/
[data-theme='dark'] {
  --color-status-pending: #6895f4;
  --color-status-overdue: #9163d5;

  /* Raised for the same reason: a deep yellow on a dark card is a yellow nobody can find. */
  --color-status-total: #f0c04a;
}

/*
  A step is read by its colour before its label, so the colour has to say what happens to the
  control rather than whether the button is an approval. Approving a deactivation and refusing one
  sit side by side and are both approvals; the difference worth seeing from across the room is that
  one stops the control and the other leaves it running.

    go     it is, becomes, or stays live
    stop   it is not live, or this is what stops it
    wait   somebody has to decide — it is on a desk, not in a state
    late   the date it was wanted by has gone
    make   it creates something new, or is something newly created
    idle   none of the above — the fallback for a status this file has not been told about

  The tone is a variable rather than a set of finished rules so a button and a status pill can be
  the same green without either of them naming it. Whatever carries the tone decides how strongly
  to wear it: a button is a tint it can be hovered out of, a pill is a solid statement.

  Every tone is carried with a shape too — an icon on a button, a dot on a pill. Colour alone is
  not a label, and roughly one man in twelve cannot tell this green from this red.

  Written here rather than as utilities because Tailwind is compiled ahead of time from the classes
  it can see, and it does not read these script files; this one is served as it is, so a tone can be
  added without a rebuild.
*/
.tone--go   { --tone: var(--color-status-passed); }
.tone--stop { --tone: var(--color-status-failed); }
.tone--wait { --tone: var(--color-warning); }
.tone--make { --tone: var(--color-primary); }
.tone--idle { --tone: var(--color-default-500); }

/*
  The two tones that are not taken from the theme.

  A draft has to be told apart from a live control at a glance, and on several of the skins here it
  could not be: this one's primary is #1ab394 and its success is #0acf97, two teals about thirty
  units apart in sRGB, and its info is a third teal. Whichever theme colour a draft borrowed, on
  some skin it would come out the same green as Active — which is the one distinction these colours
  exist to make. A draft is the pending blue for the same reason a Pending attestation is: neither
  has been answered yet.

  Late is the other. Overdue used to wear the red, which made it a second Failed on every screen
  that showed both — and on the dashboard the two sit in the same row of tiles. It is its own
  colour now, from the palette above.
*/
.tone--new   { --tone: var(--color-status-pending); }
.tone--late  { --tone: var(--color-status-overdue); }
.tone--total { --tone: var(--color-status-total); }

/*
  Two shades come off every tone, because a tone at one strength cannot both fill a shape and be
  read on top of one.

    --tone      the colour itself, for the things that only have to be seen: a dot, a border, a wash
    --tone-ink  the same hue taken most of the way to the body text colour, for anything with words

  The greens and ambers in this theme are pale — a success can be #8abe9f — and pale ink on a wash
  of itself is the sort of thing that passes review on a bright monitor and disappears on a laptop
  at an angle. Mixing towards --color-default-900 rather than towards black is what makes this hold
  in the dark theme too: there, default-900 is the light end, so the ink lightens instead.
*/
.tone--go,
.tone--stop,
.tone--wait,
.tone--late,
.tone--make,
.tone--new,
.tone--total,
.tone--idle {
  --tone-ink: color-mix(in oklab, var(--tone) 38%, var(--color-default-900));
}

.btn-tone {
  border: 1px solid color-mix(in oklab, var(--tone) 35%, transparent);
  background: color-mix(in oklab, var(--tone) 12%, transparent);
  color: var(--tone-ink);
}

.btn-tone:hover {
  background: color-mix(in oklab, var(--tone) 22%, transparent);
  color: var(--tone-ink);
}

/* ---------- What state a control is in ---------- */

/*
  Every pill is the width of the longest label, so the column reads as one line of statuses rather
  than as ragged text of five different lengths — a mixed page is scanned down the column, and
  ragged edges are what make that slow.
*/
.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-width: 11.5rem;
  padding: 0.3rem 0.75rem;
  border-radius: 999px;
  border: 1px solid color-mix(in oklab, var(--tone) 40%, transparent);
  background: color-mix(in oklab, var(--tone) 14%, transparent);
  color: var(--tone-ink);
  font-size: 0.8125rem;
  font-weight: 500;
  line-height: 1.25;
  white-space: nowrap;
}

/* The dot is the tone at full strength — it is what carries the hue, which is why the words are
   free to be dark enough to read. */
.status-pill::before {
  content: "";
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--tone);
}

/*
  An amendment is pending approval and generating at the same time, which is the one state the
  status alone would misdescribe.

  Said inside the pill rather than beside or beneath it. Beside it, the cell is as wide as the pill
  plus the note and this column holds that width all the way down the table — a hundred rows paying
  for the handful that have one. Beneath it, the pill and the note read as two badges, which invites
  the question of which one is the status.

  So the pill divides instead: what the control is on the left, what it is doing on the right. Every
  pill is already the width of "Pending deactivation", so a shorter label leaves room that has been
  paid for and is otherwise empty — the cap goes in it. The column does not widen and the row does
  not grow.

  The cap is the pill's own tone, deeper: the pill is a 14% wash of it, the cap a good deal more of
  it. Read as one badge shaded in two, rather than as a badge with something else stuck on the end —
  and derived from --tone rather than written down, so it follows whatever pill it finds itself in
  without this rule having to know which ones can carry it.
*/
.status-pill--split {
  padding-right: 0;

  /* So the cap's fill is clipped into the pill's rounded end rather than squaring it off. */
  overflow: hidden;
}

.status-pill__live {
  display: inline-flex;
  align-items: center;

  /*
    Full height, hard against the pill's ends. Two things are needed and neither works alone: stretch
    to opt out of the pill's align-items: center, which would otherwise size the cap to its own word
    and float it in the middle; and negative vertical margins to cancel the pill's padding, which
    stretch alone only fills up to. The auto margin on the left is what pushes it to the far end.
  */
  align-self: stretch;
  margin: -0.3rem 0 -0.3rem auto;
  padding: 0 0.6rem;

  /*
    Mixed against the card rather than laid over the pill with an alpha, so the cap is one predictable
    colour instead of a wash compounded on a wash.

    No dot: every pill wears one because its 14% tint is too pale to carry the hue, and the dot is
    that hue at full strength. At this depth the block is carrying its own.
  */
  background: color-mix(in oklab, var(--tone) 45%, var(--color-card));
  color: var(--tone-ink);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* ---------- What a filter is hiding ---------- */

/*
  Stated on the page rather than only inside the drawer. A grid quietly showing a fraction of itself
  is the sort of thing somebody reports as missing data a fortnight later.

  Magenta, and written down here rather than named from the theme, for the same reason .tone--new is:
  every colour the theme offers is already saying something about a control. Green, red and amber are
  three of the five statuses, the blue at hue 264 is Draft and Pending, the purple at 300 is Overdue,
  and primary is what authoring wears. A filter is not a state a control is in — it is a state the
  page is in — so it needs a hue nothing else on this screen can be confused with. This sat at 305
  until Overdue took the purple; 340 puts forty degrees back between them, which is what it needs,
  and it stays clear of the reds down at 25.

  The ink is mixed towards --color-default-900 the way the tones are, so it lightens rather than
  darkens under the dark theme instead of sinking into its own wash.
*/
.filter-summary,
.btn.is-filtering {
  --filter-tone: oklch(0.55 0.18 340);
  --filter-ink: color-mix(in oklab, var(--filter-tone) 38%, var(--color-default-900));
}

.filter-summary {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  padding: 0.6rem 1rem;
  border-top: 1px solid var(--color-default-200);

  /* Tinted and edged in the accent rather than left the same grey as the rest of the chrome. It has
     to read as a state the page is in from the corner of an eye, not as another band of toolbar. */
  background: color-mix(in oklab, var(--filter-tone) 8%, var(--color-card));
  box-shadow: inset 3px 0 0 0 var(--filter-tone);
}

.filter-summary[hidden] { display: none; }

/* The word, the icon and the figure that go in front of the chips. */
.filter-summary__label {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--filter-ink);
  white-space: nowrap;
}

/*
  How much is being hidden, said as a number.

  "Filtered" tells somebody their list is short; "12 of 480" tells them how short, which is the
  difference between trusting the list and going looking for what is missing.
*/
.filter-summary__count {
  font-size: 0.75rem;
  font-weight: 400;
  color: var(--color-default-500);
  white-space: nowrap;
}

/* The Filters button while something is set. The badge alone is a small dot at the end of a row of
   identical grey buttons, which is not enough to answer "why is this list so short". */
.btn.is-filtering {
  border-color: color-mix(in oklab, var(--filter-tone) 45%, transparent);
  color: var(--filter-ink);
  background: color-mix(in oklab, var(--filter-tone) 10%, transparent);
}

.btn.is-filtering:hover {
  background: color-mix(in oklab, var(--filter-tone) 18%, transparent);
  color: var(--filter-ink);
}

/* The count on it, which was the theme's primary and so the one blue left in the group. */
.btn.is-filtering .badge {
  background: var(--filter-tone);
  color: var(--color-card);
}

/* Several criteria, so the chips wrap rather than pushing the Clear button off the end. */
.filter-chips {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
  flex: 1 1 auto;
}

.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  font-size: 0.75rem;

  /* The same violet as the bar they sit in, so the strip reads as one thing rather than as blue
     chips on a violet ground — and so no part of it borrows a colour a status is using. */
  background: color-mix(in oklab, var(--filter-tone) 18%, transparent);
  color: var(--color-default-900);
}

/* Each chip drops itself, so narrowing by four things and undoing one is not a trip to the drawer. */
.filter-chip__drop {
  display: inline-flex;
  padding: 0;
  border: 0;
  background: none;
  line-height: 1;
  color: var(--color-default-500);
  cursor: pointer;
}

.filter-chip__drop:hover { color: var(--color-danger); }

/* ---------- Quick date ranges, in the filter drawers ---------- */

/* Wrapping rather than scrolling: they are read by scanning for the one wanted, and a strip that
   hid half of them off the edge would send people back to typing the dates. */
.quick-range {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.quick-range__option {
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--color-default-200);
  border-radius: 0.375rem;
  background: none;
  color: var(--color-default-700);
  font-size: 0.78rem;
  line-height: 1.2;
  white-space: nowrap;
  cursor: pointer;
}

.quick-range__option:hover {
  background: var(--color-light);
  border-color: var(--color-default-300);
}

/* The range in force, in the same violet the rest of the filter furniture uses — so a drawer says
   which of the twelve it is on without borrowing a colour a status is already spending. */
.quick-range__option[aria-pressed='true'] {
  background: color-mix(in oklab, var(--filter-tone) 16%, transparent);
  border-color: color-mix(in oklab, var(--filter-tone) 45%, transparent);
  color: var(--color-default-900);
  font-weight: 500;
}

/* ---------- Saved views, in the filter drawers ---------- */

.saved-views {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.saved-views__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.saved-views__title {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--color-default-700);
}

.saved-views__save {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--filter-tone);
  font-size: 0.75rem;
  cursor: pointer;
}

.saved-views__save:hover { text-decoration: underline; }

.saved-views__empty,
.saved-views__note {
  margin: 0;
  font-size: 0.72rem;
  color: var(--color-default-400);
}

.saved-views__note { color: var(--color-danger); }

.saved-views__list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

/* A view and its remove sit in one bordered box, so the x reads as belonging to that view rather
   than as another view in the row. */
.saved-view {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--color-default-200);
  border-radius: 999px;
  overflow: hidden;
}

.saved-view__open {
  padding: 0.25rem 0.2rem 0.25rem 0.7rem;
  border: 0;
  background: none;
  color: var(--color-default-700);
  font-size: 0.78rem;
  line-height: 1.2;
  cursor: pointer;
}

.saved-view__drop {
  display: inline-flex;
  padding: 0.25rem 0.5rem 0.25rem 0.3rem;
  border: 0;
  background: none;
  line-height: 1;
  color: var(--color-default-400);
  cursor: pointer;
}

.saved-view:hover { border-color: var(--color-default-300); }
.saved-view:hover .saved-view__open { color: var(--color-default-900); }
.saved-view__drop:hover { color: var(--color-danger); }

/* The one the list is actually showing. */
.saved-view--on {
  background: color-mix(in oklab, var(--filter-tone) 16%, transparent);
  border-color: color-mix(in oklab, var(--filter-tone) 45%, transparent);
}

.saved-view--on .saved-view__open {
  color: var(--color-default-900);
  font-weight: 500;
}

/* ---------- Holiday list (Config > Calendars) ---------- */

.holiday-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.holiday-row {
  display: grid;
  grid-template-columns: 6rem 2.5rem 1fr auto auto;
  gap: 0.5rem;
  align-items: center;
  padding: 0.4rem 0.2rem;
  border-bottom: 1px solid var(--color-default-200);
  font-size: 0.82rem;
}

.holiday-row:last-child { border-bottom: 0; }

.holiday-row__date {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.76rem;
  color: var(--color-default-700);
}

.holiday-row__day {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.7rem;
  color: var(--color-default-400);
}

.holiday-row__name {
  color: var(--color-default-700);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.holiday-row__tag {
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-default-400);
  border: 1px solid var(--color-default-300);
  border-radius: 999px;
  padding: 0.05rem 0.45rem;
}

/* Hand-entered dates survive a re-import, so they are worth telling apart at a glance. */
.holiday-row__tag--manual {
  color: var(--color-primary);
  border-color: var(--color-primary);
}

/* ---------- Sortable grid header and search ---------- */

th[data-sort] {
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}

th[data-sort]:hover { color: var(--color-default-700); }

/* The caret only appears on the sorted column, so the header row stays quiet. */
th[data-sort]::after {
  content: '';
  display: inline-block;
  width: 0;
  height: 0;
  margin-inline-start: 0.35rem;
  vertical-align: middle;
  border-inline: 0.28rem solid transparent;
  opacity: 0;
}

th[data-sort].is-sorted {
  color: var(--color-primary);
}

th[data-sort][data-direction="asc"]::after {
  border-bottom: 0.32rem solid currentcolor;
  opacity: 1;
}

th[data-sort][data-direction="desc"]::after {
  border-top: 0.32rem solid currentcolor;
  opacity: 1;
}

th[data-sort]:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: -2px;
}

.grid-search { position: relative; }

.grid-search__icon {
  position: absolute;
  inset-inline-start: 0.6rem;
  top: 50%;
  transform: translateY(-50%);
  color: var(--color-default-400);
  pointer-events: none;
}

.grid-search .form-input {
  padding-inline-start: 2rem;
  min-width: 15rem;
}

/* ---------- The line under a grid: which rows, and how to reach the others ---------- */

.grid-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding: 0.5rem 1rem 0.75rem;
}

.grid-pager {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

.grid-pager[hidden] { display: none; }

.grid-pager__step {
  min-width: 1.9rem;
  padding: 0.2rem 0.45rem;
  border: 1px solid var(--color-default-300);
  border-radius: 0.25rem;
  background: none;
  color: var(--color-default-600);
  font-size: 0.85rem;
  line-height: 1.4;
  cursor: pointer;
}

.grid-pager__step:hover:not(:disabled) {
  background: var(--color-light);
  color: var(--color-default-900);
}

/* Still readable at the ends of the list, but plainly not a button any more. */
.grid-pager__step:disabled {
  opacity: 0.4;
  cursor: default;
}

.grid-pager__where {
  padding-inline: 0.5rem;
  color: var(--color-default-500);
  font-size: 0.75rem;
  white-space: nowrap;
}

/* ---------- Rows that open on double-click, or pick themselves out on one ---------- */

/* The cursor is the only hint that the row itself does something. */
.row-openable,
.row-selectable { cursor: pointer; }

/* A picked row is tinted off its own ticked box — see .table:has(.form-checkbox:checked) in
   custom/_table.css — so nothing here has to say it twice. */

/* ---------- Frequencies grid ---------- */

/* Pattern sentences are long; the grid stays readable and the full text is in the export. */
.frequency-pattern {
  display: inline-block;
  max-width: 34rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}

.frequency-date {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.78rem;
  color: var(--color-default-700);
}

/* ---------- Hierarchy tree (Config > Hierarchies) ---------- */

/*
  hierarchy-tree.css carries its own palette so the component stands alone. Inside the portal the
  theme belongs to the portal, so its custom properties are re-pointed at the portal's tokens here and
  light, dark and every skin follow without the component knowing which one is active.

  The properties go on the component's own root, inside the mount, and not on the mount itself.
  Inheritance is no use here: the component declares all of these on `.hierarchy-tree` as its
  defaults, and a value set on the element always beats one inherited from its parent, so a mapping
  on the mount would be silently ignored. `.hierarchy-tree` is the documented retheming hook rather
  than an internal detail, and the id in front of it outranks the component's own light and dark
  rules in both directions.

  Only the values the portal theme says better than the component's own defaults are re-pointed. The search
  highlight and the row background are left alone: there is no theme token for a highlight, and a
  row sitting on the card wants the card's colour, which is what transparent already gives it.
*/

#hierarchy-tree .hierarchy-tree {
  --hierarchy-tree-accent: var(--color-primary);
  --hierarchy-tree-accent-contrast: var(--color-white);
  --hierarchy-tree-danger: var(--color-danger);
  --hierarchy-tree-text: var(--color-default-800);
  --hierarchy-tree-text-muted: var(--color-default-500);
  --hierarchy-tree-text-faint: var(--color-default-400);
  --hierarchy-tree-border: var(--color-default-300);
  --hierarchy-tree-guide: var(--color-default-200);
  --hierarchy-tree-surface-hover: var(--color-light);

  /* Tinted rather than the theme's light grey, so the current row still reads as current under the
     pointer — hover is the light grey, and the two would otherwise be the same colour. */
  --hierarchy-tree-surface-selected: color-mix(in oklab, var(--color-primary) 14%, transparent);

  /* The context menu floats over the page, so it needs an opaque colour of its own. The theme's own
     dropdowns use bg-card, and the component's default is the system window colour, which would
     stay white under a dark skin. */
  --hierarchy-tree-menu-surface: var(--color-card);

  --hierarchy-tree-radius: 0.375rem;
  --hierarchy-tree-font: inherit;
  --hierarchy-tree-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* The card keeps its shape while the tree loads, or while a search prunes it to nothing. */
#hierarchy-tree {
  min-height: 12rem;
}

/* ============================================================================
   Control library

   The tab strip, the multi-select pickers, the node pickers on the Details tab
   and the history timeline. All of it is built or driven at runtime by
   control-library.js and control-editor.js, so it lives here rather than in the
   Tailwind build, on the same terms as everything above: the portal's own tokens,
   so every skin follows without this file knowing which is active.
   ============================================================================ */

/* ---------- Tab strip ---------- */

.control-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.125rem;
  width: 100%;
  padding: 0.25rem 0.5rem 0;
  border-bottom: 1px solid var(--color-default-200);
}

.control-tabs button {
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  padding: 0.7rem 0.85rem;
  font-size: 0.82rem;
  font-weight: 500;
  color: var(--color-default-500);
  cursor: pointer;
  white-space: nowrap;

  /* The strip is the one thing on the page that moves as you work through a
     control, so the underline slides rather than snapping. */
  transition: color 0.15s ease, border-color 0.15s ease;
}

.control-tabs button:hover {
  color: var(--color-default-800);
}

.control-tabs button[aria-selected='true'] {
  color: var(--color-primary);
  border-bottom-color: var(--color-primary);
}

.control-tabs button:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: -2px;
  border-radius: 0.25rem 0.25rem 0 0;
}

/* A tab whose fields failed validation, so a save that is refused says where. */
.control-tabs button[data-invalid='true'] {
  color: var(--color-danger);
}

.control-tabs button[data-invalid='true']::after {
  content: '';
  display: inline-block;
  width: 0.35rem;
  height: 0.35rem;
  margin-inline-start: 0.35rem;
  border-radius: 50%;
  background: var(--color-danger);
  vertical-align: 0.15em;
}

/* The Hierarchy / Geography pair inside the Details tab. Smaller and quieter,
   because it selects between two halves of one field rather than two tabs. */
.control-subtabs {
  display: flex;
  gap: 0.25rem;
  margin-bottom: 0.75rem;
  border-bottom: 1px solid var(--color-default-200);
}

.control-subtabs button {
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  padding: 0.45rem 0.7rem;
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--color-default-500);
  cursor: pointer;
}

.control-subtabs button[aria-selected='true'] {
  color: var(--color-default-900);
  border-bottom-color: var(--color-primary);
}

/* ---------- Node pickers on the Details tab ---------- */

.control-tree {
  max-height: 22rem;
  overflow: auto;
  border: 1px solid var(--color-default-200);
  border-radius: 0.375rem;
  padding: 0.4rem;
  font-size: 0.82rem;
}

.control-tree__empty {
  color: var(--color-default-400);
  padding: 1.5rem 0.5rem;
  text-align: center;
}

.control-tree__node {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  width: 100%;
  padding: 0.3rem 0.4rem;
  border: 0;
  border-radius: 0.25rem;
  background: none;
  color: var(--color-default-800);
  text-align: start;
  cursor: pointer;
}

.control-tree__node:hover {
  background: var(--color-light);
}

.control-tree__node[aria-pressed='true'] {
  background: color-mix(in oklab, var(--color-primary) 14%, transparent);
  color: var(--color-default-900);
  font-weight: 500;
}

.control-tree__node i {
  color: var(--color-default-400);
  flex: none;
}

/* Depth is drawn with padding rather than nesting, so one flat list in path
   order is enough to render the whole tree. */
.control-tree__node[data-level='1'] { padding-inline-start: 0.4rem; }
.control-tree__node[data-level='2'] { padding-inline-start: 1.4rem; }
.control-tree__node[data-level='3'] { padding-inline-start: 2.4rem; }
.control-tree__node[data-level='4'] { padding-inline-start: 3.4rem; }
.control-tree__node[data-level='5'] { padding-inline-start: 4.4rem; }
.control-tree__node[data-level='6'] { padding-inline-start: 5.4rem; }

/* ---------- Multi-select pickers ---------- */

.picker {
  border: 1px solid var(--color-default-200);
  border-radius: 0.375rem;
  padding: 0.5rem;
}

.picker__chosen {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  min-height: 1.75rem;
  margin-bottom: 0.5rem;
}

.picker__empty {
  color: var(--color-default-400);
  font-size: 0.8rem;
  align-self: center;
}

.picker__tag {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem 0.5rem;
  border-radius: 999px;
  font-size: 0.75rem;
  background: color-mix(in oklab, var(--color-primary) 14%, transparent);
  color: var(--color-default-900);
}

.picker__tag button {
  appearance: none;
  border: 0;
  background: none;
  padding: 0;
  line-height: 1;
  color: var(--color-default-500);
  cursor: pointer;
}

.picker__tag button:hover {
  color: var(--color-danger);
}

.picker__options {
  max-height: 14rem;
  overflow: auto;
  border-top: 1px dashed var(--color-default-200);
  padding-top: 0.4rem;
}

.picker__option {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  width: 100%;
  padding: 0.3rem 0.4rem;
  border: 0;
  border-radius: 0.25rem;
  background: none;
  color: var(--color-default-800);
  font-size: 0.82rem;
  text-align: start;
  cursor: pointer;
}

.picker__option:hover {
  background: var(--color-light);
}

.picker__option[aria-pressed='true'] {
  color: var(--color-default-400);
  cursor: default;
}

.picker__option-note {
  color: var(--color-default-400);
  font-size: 0.72rem;
}

/* ---------- Searchable dropdowns ---------- */

/*
  The box wears whatever classes the select it replaced was wearing, so it is a .form-select and
  needs no colours of its own. Only the list below it is new, and it borrows the multi-select
  picker's option styling so the two read as the same idea.
*/

.combo { position: relative; }

/*
  Still the value holder, still submitted, just never seen.

  Written as a descendant so it outranks the .form-select and .form-input it is competing with:
  those come from app.min.css, which the layout loads after this file, and at equal specificity the
  later rule would win and leave the select sitting there at its full width.
*/
.combo .combo__native {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.combo__list {
  position: fixed;
  z-index: 1060;
  overflow-y: auto;
  margin: 0;
  padding: 0.25rem;
  list-style: none;
  background: var(--color-card);
  border: 1px solid var(--color-default-200);
  border-radius: 0.375rem;
  box-shadow: 0 10px 25px -5px rgb(0 0 0 / 25%);
}

.combo__list[hidden] { display: none; }

/*
  A box inside a question, whose panel is .run at 1070. Above it, because here the list is part of
  what is being answered rather than something left open on the page behind it. combo-box.js decides
  which of the two a list is, on every open.
*/
.combo__list--over-dialog { z-index: 1080; }

.combo__option {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.35rem 0.5rem;
  border-radius: 0.25rem;
  font-size: 0.82rem;
  color: var(--color-default-800);
  cursor: pointer;
}

/* One highlight for both the pointer and the arrow keys, so they cannot disagree. */
.combo__option:hover,
.combo__option.is-active {
  background: var(--color-light);
}

.combo__option[aria-selected='true'] {
  color: var(--color-primary);
  font-weight: 500;
}

.combo__note {
  margin-inline-start: auto;
  padding-inline-start: 0.5rem;
  color: var(--color-default-400);
  font-size: 0.72rem;
  white-space: nowrap;
}

/* Not an option: the count of what is not being shown, or why nothing is. */
.combo__message {
  padding: 0.4rem 0.5rem;
  color: var(--color-default-400);
  font-size: 0.72rem;
}

/* ---------- History timeline ---------- */

.timeline {
  list-style: none;
  margin: 0;
  padding: 0;
}

.timeline li {
  position: relative;
  padding: 0 0 1.1rem 1.5rem;
  border-inline-start: 1px solid var(--color-default-200);
}

.timeline li:last-child {
  border-inline-start-color: transparent;
  padding-bottom: 0;
}

.timeline li::before {
  content: '';
  position: absolute;
  inset-inline-start: -0.3rem;
  top: 0.35rem;
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: var(--color-default-300);
}

/* The entries that changed the control's standing, rather than its wording. */
.timeline li[data-decisive='true']::before {
  background: var(--color-primary);
}

.timeline__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  font-size: 0.85rem;
  color: var(--color-default-800);
}

.timeline__when {
  color: var(--color-default-400);
  font-size: 0.72rem;
}

.timeline__comment {
  margin: 0.35rem 0 0;
  font-size: 0.8rem;
  color: var(--color-default-600);
  font-style: italic;
}

.timeline__changes {
  margin: 0.5rem 0 0;
  border-collapse: collapse;
  font-size: 0.78rem;
  width: 100%;
}

.timeline__changes th {
  text-align: start;
  font-weight: 500;
  color: var(--color-default-400);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 0 0.6rem 0.25rem 0;
}

.timeline__changes td {
  padding: 0.15rem 0.6rem 0.15rem 0;
  vertical-align: top;
  color: var(--color-default-700);
}

.timeline__changes td:first-child {
  color: var(--color-default-500);
  white-space: nowrap;
}

/* Struck through rather than coloured red: a superseded value is not an error. */
.timeline__old {
  text-decoration: line-through;
  color: var(--color-default-400);
}

/*
  ---------- An attestation's history ----------

  The library's timeline above is a list of edits; this is a route. An attestation is answered
  rather than amended, so what its history has to show is where each step took it — and the steps
  that took it nowhere, a file attached or removed, should not compete with the ones that did.

  So the dot carries the tone of the status the step landed on, and only for the steps that moved
  it: the eye runs down the left edge and reads pending, amber, green without reading a word. The
  rest keep the grey dot they had. Same timeline, same spacing, same everything else.
*/

.timeline--history li[data-decisive='true']::before {
  background: var(--tone, var(--color-primary));

  /* A ring, so a green dot on a green-ish theme is still a dot with an edge. */
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--tone, var(--color-primary)) 22%, transparent);
}

/* Filtered out by the search below. Said here because .timeline li sets position, and a rule with a
   class beats the browser's own [hidden] the moment anything else in this file grows a display. */
.timeline--history li[hidden] {
  display: none;
}

.timeline__who {
  color: var(--color-default-500);
  font-size: 0.8rem;
}

/* Where the step took it. Muted, because it repeats what the action label mostly says — it is here
   for the reader who wants the exact transition, not for the one skimming. */
.timeline__move {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  margin-top: 0.2rem;
  font-size: 0.72rem;
  color: var(--color-default-400);
}

.timeline__arrow {
  font-size: 0.85rem;
  opacity: 0.7;
}

/* The end of the move is where it is now, so it gets the weight and the tone the dot is wearing. */
.timeline__to {
  color: var(--tone-ink, var(--color-default-600));
  font-weight: 500;
}

/*
  The comment is rendered markdown here rather than the plain string the library's timeline shows,
  so it cannot be italicised wholesale — a bulleted list in italics reads as a mistake. Set off with
  a rule instead, which is what a quotation wants anyway.
*/
.timeline--history .timeline__comment {
  margin: 0.4rem 0 0;
  padding-inline-start: 0.6rem;
  border-inline-start: 2px solid var(--color-default-200);
  font-style: normal;
  color: var(--color-default-600);
}

/* ---------- Searching and copying one ---------- */

/* The heading and the copy button on one line, the button hard right. */
.detail__heading-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.6rem;
}

.timeline__search {
  margin-bottom: 0.4rem;
}

/* Full width of the panel rather than the grid box's 15rem, which is a column width and not one. */
.timeline__search .form-input {
  min-width: 0;
  width: 100%;
}

.timeline__count {
  margin: 0 0 0.6rem;
  font-size: 0.7rem;
  color: var(--color-default-400);
}

/*
  The strip immediately above the entries: how many of them there are, and the button that copies
  them.

  Copy all sat in the heading row, which put it a search box away from the thing it copies and next
  to a heading it has nothing to do with. Here it is against the list, beside the figure that says
  what a search has left showing — which is exactly what it will hand over.

  Justified to the end rather than spaced apart, so the button stays right whether or not the count
  is there; the count pushes itself left instead. With three entries or fewer there is no search and
  no count, and the bar is the button alone.
*/
.timeline__bar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.75rem;
  margin-bottom: 0.6rem;
}

.timeline__bar .timeline__count {
  margin: 0 auto 0 0;
}

/*
  A pill inside a line of text rather than in a column of them.

  .status-pill is 11.5rem wide so that a column of them lines up in the grids. In a timeline head
  that is most of the width doing nothing, so this one is sized to its words.
*/
.status-pill--inline {
  min-width: 0;
  padding: 0.1rem 0.5rem;
  font-size: 0.68rem;
}

/* ---------- The Frequency tab's preview ---------- */

/* The .dates rows above are a three-column grid — sequence, date, weekday — built for the
   standalone builder. A control's preview is one sentence per row, which in that grid lands in the
   2.5rem first column and wraps to five lines. Same list, different row. */
.dates li.frequency-date {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  font-family: inherit;
  font-size: 0.82rem;
  white-space: nowrap;
}

/* ---------- Status tiles on the library ---------- */

/*
  Each tile wears the tone of the status it counts — the same one that status wears as a pill in the
  grid below, so the thing clicked and the rows it produces are recognisably the same thing.

  Worn lightly: a bar down the leading edge and the number in the tone's ink, over a card that stays
  the colour of every other card. Five tinted blocks across the top of the page would compete with
  the grid, which is what anyone came here to read.
*/
.status-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  width: 100%;
  padding: 1rem 1.15rem;
  border: 1px solid var(--color-default-200);
  border-radius: 0.5rem;
  background: var(--color-card);
  text-align: start;
  cursor: pointer;
  overflow: hidden;   /* so the bar takes the card's rounded corners rather than squaring them */
}

.status-tile::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 3px;
  background: var(--tone);
}

.status-tile:hover {
  border-color: color-mix(in oklab, var(--tone) 55%, transparent);
}

/* Selected is the tone as well, so which tile is filtering does not change colour to say so. */
.status-tile[aria-pressed='true'] {
  border-color: color-mix(in oklab, var(--tone) 55%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--tone) 55%, transparent);
  background: color-mix(in oklab, var(--tone) 8%, var(--color-card));
}

.status-tile__count {
  font-size: 1.4rem;
  font-weight: 600;
  color: var(--tone-ink);
  line-height: 1.2;
}

.status-tile__label {
  font-size: 0.75rem;
  color: var(--color-default-500);
}

/* ---------- A generated password, shown once ---------- */

.generated-password {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin: 0.75rem 0 0.25rem;
  padding: 0.75rem 0.9rem;
  border: 1px dashed var(--color-default-300);
  border-radius: 0.375rem;
  background: var(--color-light);
}

.generated-password code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1rem;
  letter-spacing: 0.04em;
  color: var(--color-default-900);
  word-break: break-all;

  /* Selectable by design: the clipboard API is refused over plain HTTP, and picking the value out
     by hand has to stay possible when the copy button cannot do it. */
  user-select: all;
}

/* ============================================================================
   Alerts

   The theme ships no alert component — `alert-triangle` in its CSS is an icon
   name, not a class — so every `class="alert alert-warning"` in these views was
   rendering as unstyled body text. This defines one.

   A left accent bar rather than a full border or a solid fill: it marks the
   message down the side where the eye is already travelling, holds its meaning
   at a glance from the colour alone, and stays legible against both a card and
   a dialog without competing with either. The tint is mixed from the same token
   as the bar, so a skin that repaints danger repaints both.
   ============================================================================ */

.alert {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.7rem 0.9rem;
  border-radius: 0.375rem;

  /* Logical, so the bar follows the reading direction rather than always sitting
     on the left of a right-to-left page. */
  border-inline-start: 3px solid var(--alert-accent);
  background: color-mix(in oklab, var(--alert-accent) 10%, transparent);
  color: var(--color-default-800);

  font-size: 0.82rem;
  line-height: 1.45;
  text-wrap: pretty;

  /* Defaults to the neutral tone, so a bare `class="alert"` is still a legible
     message rather than an invisible one. */
  --alert-accent: var(--color-default-400);
}

/* The pages toggle these with the hidden attribute, and display above would
   otherwise win. Scoped to the component so it works in a dialog too, where the
   page-level [hidden] rule does not reach. */
.alert[hidden] {
  display: none !important;
}

.alert-danger { --alert-accent: var(--color-danger); }
.alert-success { --alert-accent: var(--color-success); }
.alert-warning { --alert-accent: var(--color-warning); }
.alert-info { --alert-accent: var(--color-info); }
.alert-primary { --alert-accent: var(--color-primary); }

/* The message itself carries the colour only where it is the whole point — an
   error — so a long informational note stays comfortable to read. */
.alert-danger { color: var(--color-default-900); }

.alert strong { font-weight: 600; }

.alert code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.95em;
  padding: 0.05rem 0.25rem;
  border-radius: 0.2rem;
  background: color-mix(in oklab, var(--color-default-900) 8%, transparent);
}

.alert a {
  color: var(--color-primary);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Sits flush inside a card body or a table footer, where the surrounding
   padding has already been applied. */
.alert--flush {
  margin: 0 1rem 1rem;
}

/* ASP.NET's validation summary renders a list inside whatever it is given, so the alert absorbs it
   rather than showing a bulleted list indented away from its own accent bar. */
.alert ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.alert li + li {
  margin-top: 0.3rem;
}

/* ModelOnly omits the wrapper entirely when there is nothing to say, so this does not fire today.
   It is here because asp-validation-summary="All" does render an empty one, and a coloured bar with
   nothing in it is the sort of thing that appears the day somebody changes that attribute. */
.alert.validation-summary-valid {
  display: none;
}

/* ==========================================================================
   The dashboard
   ========================================================================== */

/*
  One filter row, above everything it narrows.

  Not a filter per card. Every figure on the page is the same set counted a different way, so they
  are narrowed together or they would quietly stop being comparable with each other.
*/
.dash-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
}

.dash-bar__spacer {
  flex: 1 1 auto;
}

/* The queue's tile, as a link — because on this page every figure opens the rows behind it. */
.dash-tile {
  text-decoration: none;
  gap: 0.25rem;
}

.dash-tile:hover .status-tile__count {
  text-decoration: underline;
}

.dash-tile__note {
  font-size: 0.7rem;
  color: var(--color-default-400);
  line-height: 1.35;
  margin-top: 0.15rem;
}

/*
  The one number the page leads with.

  Proportional figures rather than tabular: equal-width digits are for numbers that line up under
  each other in a table, and at this size they make a percentage look loose.
*/
.dash-hero {
  font-size: 2.75rem;
  font-weight: 600;
  line-height: 1.1;
  color: var(--color-default-900);
  margin: 0;
}

.dash-delta {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.8rem;
  color: var(--tone-ink);
  margin: 0.35rem 0 0;
}

.dash-split {
  display: flex;
  gap: 0.75rem;
  margin-top: 1.25rem;
}

.dash-count {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--color-default-200);
  border-inline-start: 3px solid var(--tone);
  border-radius: 0.375rem;
  text-decoration: none;
}

.dash-count:hover {
  border-color: color-mix(in oklab, var(--tone) 55%, transparent);
  border-inline-start-color: var(--tone);
}

.dash-count__value {
  font-size: 1.1rem;
  font-weight: 600;
  color: var(--tone-ink);
}

.dash-count__label {
  font-size: 0.7rem;
  color: var(--color-default-500);
}

/*
  The overdue pile, by how long it has been sitting.

  One hue getting stronger rather than three colours: the bands are ordered, so a ramp says which is
  worse without a legend to explain it. The count is printed beside every bar — the numbers are the
  point, and there are only three of them.
*/
.dash-band {
  display: grid;
  grid-template-columns: 10rem 1fr auto;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0;
  text-decoration: none;
}

.dash-band + .dash-band {
  border-top: 1px solid var(--color-default-200);
}

.dash-band__label {
  font-size: 0.8rem;
  color: var(--color-default-600);
}

.dash-band__track {
  block-size: 0.5rem;
  border-radius: 0.25rem;
  background: var(--color-default-100);
  overflow: hidden;
}

.dash-band__fill {
  display: block;
  block-size: 100%;
  border-radius: 0.25rem;
  background: var(--color-status-overdue);
}

/* Mixed towards the card rather than towards white, so the ramp holds in the dark theme too. */
.dash-band__fill[data-strength='1'] {
  background: color-mix(in oklab, var(--color-status-overdue) 45%, var(--color-card));
}

.dash-band__fill[data-strength='2'] {
  background: color-mix(in oklab, var(--color-status-overdue) 70%, var(--color-card));
}

.dash-band__value {
  font-size: 0.9rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--color-default-900);
  min-inline-size: 2.5rem;
  text-align: end;
}

.dash-band:hover .dash-band__label,
.dash-band:hover .dash-band__value {
  color: var(--color-primary);
}

/* A chart sized to include its axis labels, so the card does not grow a nested scrollbar. */
.dash-chart {
  min-block-size: 1rem;
}

.dash-table {
  margin: 0;
}

.dash-row {
  cursor: pointer;
}

.dash-row:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: -2px;
}

/* A figure worth noticing wears the tone of what it counts; a zero is left as ordinary text. */
.dash-figure {
  font-weight: 600;
  color: var(--tone-ink);
}

/*
  The trail through the organisation.

  Between the card's header and its chart rather than inside either, because it is neither a title
  nor part of the drawing: it says where the chart currently is, and it is how somebody gets back
  out. Every level but the last is a button; the last is where you are, so it is inert.
*/
.dash-crumbs {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex-wrap: wrap;
  padding: 0.65rem 1.15rem;
  border-block-start: 1px solid var(--color-default-200);
  border-block-end: 1px solid var(--color-default-200);
  background: var(--color-default-100);
}

.dash-crumb {
  font-size: 0.78rem;
  color: var(--color-primary);
  background: none;
  border: 0;
  padding: 0.1rem 0.2rem;
  border-radius: 0.25rem;
}

.dash-crumb:hover:not(:disabled) {
  background: color-mix(in oklab, var(--color-primary) 12%, transparent);
}

/* Where you already are. Still readable, and plainly not somewhere to go. */
.dash-crumb:disabled {
  color: var(--color-default-600);
  font-weight: 600;
  cursor: default;
}

.dash-crumb__sep {
  color: var(--color-default-400);
  font-size: 0.78rem;
}

/* Clicking a bar goes deeper, so this is the way from here to the rows themselves. */
.dash-crumb__open {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  margin-inline-start: auto;
  font-size: 0.75rem;
  color: var(--color-default-500);
  text-decoration: none;
}

.dash-crumb__open:hover {
  color: var(--color-primary);
}

/* The chevron that says a row of the branch table has another level under it. */
.dash-row__in {
  margin-inline-start: 0.35rem;
  color: var(--color-default-400);
  vertical-align: -0.1em;
}

.dash-row:hover .dash-row__in {
  color: var(--color-primary);
}

/*
  Going a level in.

  Two states, and both are about the same thing: the numbers on screen stay the last true answer
  until the next one lands. Nothing is emptied and nothing is replaced by a spinner — a chart that
  blanks and comes back reads as a failure even when it is a success.

    is-drilling  the moment after a click, letting the bars finish falling before the next
                 level's arrive. Overlapping the two animations gives a flicker rather than
                 a descent.
    is-loading   while the level is being fetched.

  Held back at a whole-card level rather than per bar, because what is changing is which level you
  are looking at, not which numbers moved.
*/
.dash-drill .card-body,
.dash-drill .dash-crumbs {
  transition: opacity 0.14s ease-out;
}

.dash-drill.is-drilling .card-body {
  opacity: 0.35;
}

.dash-drill.is-loading .card-body,
.dash-drill.is-loading .dash-crumbs {
  opacity: 0.55;
}

/* The crumb that has just been added, arriving rather than appearing. */
.dash-crumbs .dash-crumb:last-of-type:not(:first-of-type) {
  animation: dash-crumb-in 0.22s ease-out;
}

@keyframes dash-crumb-in {
  from { opacity: 0; transform: translateX(-0.35rem); }
  to   { opacity: 1; transform: none; }
}

/*
  Somebody who has asked their system not to animate has asked this page too. The chart's own
  transitions are turned off in the script beside this, which reads the same preference — Apex does
  not read it itself. Every figure stays on screen either way; it simply arrives rather than
  travels.
*/
@media (prefers-reduced-motion: reduce) {
  .dash-drill .card-body,
  .dash-drill .dash-crumbs {
    transition: none;
  }

  .dash-drill.is-drilling .card-body {
    opacity: 1;
  }

  .dash-crumbs .dash-crumb:last-of-type:not(:first-of-type) {
    animation: none;
  }
}

/*
  Back, beside the trail rather than instead of it.

  The trail is how somebody jumps anywhere; this is how they go back one, which is what they
  actually do most of the time. Set apart by a rule so it reads as a control rather than as the
  first crumb.
*/
.dash-crumb__back {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.75rem;
  color: var(--color-default-600);
  background: var(--color-card);
  border: 1px solid var(--color-default-300);
  border-radius: 0.3rem;
  padding: 0.15rem 0.5rem 0.15rem 0.35rem;
}

.dash-crumb__back:hover:not(:disabled) {
  border-color: color-mix(in oklab, var(--color-primary) 45%, transparent);
  color: var(--color-primary);
}

/* At the top of the tree there is nowhere back to. Dimmed rather than removed, so the row of
   controls does not change shape as somebody walks up and down. */
.dash-crumb__back:disabled {
  opacity: 0.4;
  cursor: default;
}

.dash-crumbs__rule {
  inline-size: 1px;
  block-size: 1rem;
  background: var(--color-default-300);
  margin-inline: 0.15rem 0.35rem;
}

/* ---------- The figure tiles ----------

   A headline and the parts it is made of. Each line is a link, so the whole tile is a way in to the
   rows rather than a display: the label says what, the figure says how many, and clicking either
   opens exactly that set in the queue.

   The label is a filled pill and the figure is plain dark text, which is the division the design
   guidance asks for — colour identifies, ink is read. A row of tinted blocks would compete with the
   chart underneath, which is the thing on this page worth looking at.
*/
.fig-tile {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--color-default-200);
  border-radius: 0.5rem;
  background: var(--color-card);
  overflow: hidden;
}

.fig-tile__head,
.fig-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  text-decoration: none;
}

.fig-tile__head {
  padding: 0.9rem 1rem;
  border-block-end: 1px solid var(--color-default-200);
}

.fig-tile__head:hover,
.fig-row:hover {
  background: color-mix(in oklab, var(--tone) 7%, transparent);
}

/* Proportional figures, not tabular: these are headline numbers rather than a column that lines up
   under itself, and equal-width digits make them look loose at this size. */
.fig-tile__value {
  font-size: 1.6rem;
  font-weight: 600;
  line-height: 1.1;
  color: var(--color-default-900);
}

.fig-tile__rows {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.6rem 0.5rem;
  flex: 1 1 auto;
}

.fig-row {
  padding: 0.3rem 0.5rem;
  border-radius: 0.3rem;
}

.fig-row__value {
  font-size: 0.95rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;   /* these do line up under each other */
  color: var(--color-default-700);
}

/* The label, wearing its tone as a fill. --tone-ink is the same hue taken most of the way to the
   body text colour, which is what keeps a pale success or amber readable on a wash of itself. */
.fig-pill {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border-radius: 0.3rem;
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1.4;
  color: var(--tone-ink);
  background: color-mix(in oklab, var(--tone) 20%, transparent);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.fig-pill--sm {
  font-size: 0.68rem;
  font-weight: 500;
  padding: 0.1rem 0.4rem;
}

/* What the figures are about. A number without a place is not an answer to anything, and this
   page's place moves as somebody walks the chart below it. */
.fig-tile__foot {
  padding: 0.5rem 1rem;
  border-block-start: 1px solid var(--color-default-200);
  text-align: center;
  font-size: 0.7rem;
  color: var(--color-default-400);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---------- How long it takes, and what is waiting ----------

   The same band the overdue pile is drawn with, in whichever tone the thing being counted wears.
   Reusing it is the point: a reader who has understood one row of bars on this page has understood
   all of them, and a second bar component would only differ by accident.
*/
.dash-band__fill[data-tone='primary'] { background: var(--color-primary); }
.dash-band__fill[data-tone='warning'] { background: var(--color-warning); }

.dash-band__fill[data-tone='primary'][data-strength='1'] {
  background: color-mix(in oklab, var(--color-primary) 45%, var(--color-card));
}

.dash-band__fill[data-tone='primary'][data-strength='2'] {
  background: color-mix(in oklab, var(--color-primary) 70%, var(--color-card));
}

.dash-band__fill[data-tone='warning'][data-strength='1'] {
  background: color-mix(in oklab, var(--color-warning) 45%, var(--color-card));
}

.dash-band__fill[data-tone='warning'][data-strength='2'] {
  background: color-mix(in oklab, var(--color-warning) 70%, var(--color-card));
}

/* These bands are figures rather than links — there is no filter for "took eleven days to answer",
   and a link that landed on something else would be worse than the figures stopping here. So no
   hover, and the label stays the colour it started. */
.dash-band--flat {
  cursor: default;
}

.dash-band--flat .dash-band__label {
  grid-column: 1;
}

.dash-figure-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.35rem;
}

.dash-figure-head__title {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--color-default-700);
  margin: 0;
}

.dash-figure-head__note {
  font-size: 0.72rem;
  color: var(--color-default-400);
}

/* The one way out of a card of figures, where the individual bands have none. */
.dash-band__all {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  margin-top: 0.85rem;
  font-size: 0.75rem;
  color: var(--color-default-500);
  text-decoration: none;
}

.dash-band__all:hover {
  color: var(--color-primary);
}

/* "4 of 7", with what that comes to underneath. The count alone cannot tell a daily control having
   a bad fortnight from a monthly one that has never worked. */
.dash-rate {
  display: block;
  font-size: 0.68rem;
  color: var(--color-default-400);
  margin-top: 0.1rem;
}

/* ---------- The four-eyes exemption ----------

   Nought is the answer this card hopes for, so nought is drawn as an answer rather than as an empty
   state. "Nothing to show" leaves a reader unsure whether it was checked; "nobody has" is a
   different sentence, and it is the one this says.
*/
.dash-clear {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  text-align: center;
  padding: 1.5rem 0.5rem;
}

.dash-clear__mark {
  font-size: 1.75rem;
  color: var(--color-success);
}

.dash-clear__word {
  font-size: 0.78rem;
  color: var(--color-default-500);
  margin: 0;
}

/* And when it is not nought, it wears the tone rather than shouting: this is a thing to look at,
   not a failure. The count is the link; the names under it are the way to each one. */
.dash-flag {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.7rem 0.85rem;
  border: 1px solid color-mix(in oklab, var(--tone) 35%, transparent);
  border-radius: 0.4rem;
  background: color-mix(in oklab, var(--tone) 10%, transparent);
  text-decoration: none;
}

.dash-flag:hover {
  background: color-mix(in oklab, var(--tone) 18%, transparent);
}

.dash-flag__count {
  font-size: 1.75rem;
  font-weight: 600;
  line-height: 1.05;
  color: var(--tone-ink);
}

.dash-flag__label {
  font-size: 0.7rem;
  color: var(--color-default-500);
  line-height: 1.35;
}

.dash-flag__list {
  display: flex;
  flex-direction: column;
  margin-top: 0.6rem;
}

.dash-flag__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.35rem 0.1rem;
  border-top: 1px solid var(--color-default-200);
  text-decoration: none;
}

.dash-flag__who {
  font-size: 0.76rem;
  color: var(--color-default-700);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dash-flag__what {
  font-size: 0.7rem;
  color: var(--color-default-400);
  white-space: nowrap;
}

.dash-flag__row:hover .dash-flag__who {
  color: var(--color-primary);
}

/* ---------- The line under a figure ----------

   Twelve weeks, no axis and no scale. It is not there to be read off — the number above it is the
   reading — but to say whether that number arrived or has been there all along.
*/
.fig-tile__spark {
  padding: 0 1rem;
  margin-top: -0.35rem;
}

.dash-spark {
  display: block;
  inline-size: 100%;
  block-size: 26px;
  overflow: visible;
}

.dash-spark__line {
  fill: none;
  stroke: var(--tone);
  stroke-width: 1.5;
  /* The viewBox is stretched to the tile's width, which would stretch the stroke with it. */
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.dash-spark__area {
  fill: color-mix(in oklab, var(--tone) 14%, transparent);
  stroke: none;
}

/* ---------- Half a year, a square a day ----------

   One hue getting darker, which is the only honest ramp for a magnitude: a rainbow here would say a
   busy Tuesday is a different kind of day from a quiet one. Five steps, because past about five the
   eye stops telling neighbouring shades apart and the rest is decoration.
*/
.heat {
  display: grid;
  grid-template-rows: auto auto;
  gap: 0.25rem;
  overflow-x: auto;
  padding-bottom: 0.25rem;
}

/* Down the columns then across, so a column is a week and a row is a weekday. */
.heat__grid {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(7, 0.75rem);
  grid-auto-columns: 0.75rem;
  gap: 0.15rem;
}

.heat__months {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 0.9rem;
  font-size: 0.62rem;
  color: var(--color-default-400);
  block-size: 0.9rem;
}

.heat__month {
  white-space: nowrap;
}

.heat__day {
  inline-size: 0.75rem;
  block-size: 0.75rem;
  border-radius: 0.15rem;
  background: var(--color-default-200);
}

.heat__day[data-level='1'] { background: color-mix(in oklab, var(--color-primary) 25%, var(--color-default-200)); }
.heat__day[data-level='2'] { background: color-mix(in oklab, var(--color-primary) 50%, var(--color-default-200)); }
.heat__day[data-level='3'] { background: color-mix(in oklab, var(--color-primary) 75%, var(--color-default-200)); }
.heat__day[data-level='4'] { background: var(--color-primary); }

.heat__key {
  display: flex;
  align-items: center;
  gap: 0.15rem;
  margin-top: 0.75rem;
}

.heat__key-word {
  font-size: 0.65rem;
  color: var(--color-default-400);
  margin-inline: 0.3rem;
}

/* Six tabs will not sit on one line in a half-width card. */
.control-tabs--wrap {
  flex-wrap: wrap;
}

/* ---------- Actions ---------- */

/*
  The editor is a drawer like the rest, and wider than them. What is in it is a form of two columns
  rather than a column of prose, and at the shared 26rem the two columns collapse to one on every
  screen — which is the layout the expander exists to escape rather than the one to start in.

  Over the attestation panel, not beside it: an action is raised from inside that panel and has to
  sit on top of it. 1055 rather than a second 1050, so the order does not depend on which partial a
  page happens to include first.
*/
#action-editor { z-index: 1055; }
#action-editor .drawer__panel { width: min(var(--panel-editor), 100%); }

/* One action, in the attestation panel's own tab. A button rather than a link: it opens a drawer
   over the panel rather than going anywhere. */
.detail__action {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--color-default-200);
  border-radius: var(--radius-md, 0.375rem);
}

.detail__action-open {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.1rem;
  min-width: 0;
  text-align: start;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

.detail__action-ref {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-default-400);
}

.detail__action-text {
  font-size: 0.875rem;
  color: var(--color-default-700);
}

.detail__action-open:hover .detail__action-text {
  color: var(--color-primary);
}

.detail__action-meta {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* A history entry that names an action opens it. Sized as the pills beside it so the line reads as
   one row rather than as a pill and a button. */
.timeline__link {
  cursor: pointer;
  border: 0;
  font: inherit;
}

.timeline__link:disabled {
  cursor: default;
  opacity: 0.6;
}

/* ---------- A block of the caller's own fields inside a question ---------- */

/*
  Where the failure dialog puts the action it has to carry. Ruled off above rather than boxed,
  because it is a second half of one question rather than a second question — the reader is still
  answering "did this control hold", and this is the rest of that answer.
*/
.ask__section {
  margin-top: 1rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--color-default-200);
}

.ask__section-title {
  margin: 0;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-default-500);
}

.ask__field {
  margin-top: 0.75rem;
}

.ask__field .form-select {
  width: 100%;
}

/* An owner and a date side by side at width, stacked on a phone where neither wants half of it. */
.ask__row {
  display: grid;
  gap: 0.75rem;
}

@media (min-width: 30rem) {
  .ask__row {
    grid-template-columns: 1fr 1fr;
  }
}

/*
  The AI's evidence check, under the file it is about.

  Quieter than the file itself — it is advice, and the file is the evidence. Closed, it is a score
  and a sentence; opened, the five things it looked at, each in the tone of what it found.
*/
.evidence-check {
  margin: 0 0 .5rem 1.5rem;
  font-size: .75rem;
  color: var(--color-default-500);
}

.evidence-check--note { margin-bottom: .5rem; }

.evidence-check summary {
  display: flex;
  align-items: flex-start;
  gap: .5rem;
  cursor: pointer;
  list-style: none;
}

.evidence-check summary::-webkit-details-marker { display: none; }

/* The badge keeps its width; the sentence beside it wraps instead of running under it. */
.evidence-check summary .status-pill { flex: none; white-space: nowrap; }

/*
  The queue's Evidence cell: the files attached, then which answers need one.

  The pills sit on one line where there is room and stack where there is not, and never break inside
  themselves — "To" on one line and "pass" on the next reads as two things.
*/
.evidence-cell {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem;
}

/* A status pill in colour only: not the width of the longest status, and the paperclip in place of
   the dot, since what it says is "a file", not "a state". */
.status-pill.evidence-need {
  min-width: 0;
  gap: 0.2rem;
  padding: 0.1rem 0.5rem;
  font-size: 0.75rem;
}

.status-pill.evidence-need::before { content: none; }

.status-pill.evidence-need .iconify { color: var(--tone); }


.evidence-check__summary { flex: 1; min-width: 0; line-height: 1.4; }

.evidence-check__findings {
  margin: .5rem 0 .25rem;
  padding-left: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: .35rem;
}

.evidence-check__findings li {
  padding-left: .6rem;
  border-left: 3px solid var(--tone);
  line-height: 1.4;
}

.evidence-check__by { margin: 0; font-style: italic; }

/* ---------- The risk module ---------- */

/*
  A 5×5 score is shown the same way everywhere: the number on a chip coloured by its band, with the
  band's name beside it so the colour is never the only label.

  The heat-map ramp every risk committee expects — green, yellow, orange, red. Green and red are the
  theme's passed and failed; orange is its warning. Yellow is fixed here because the theme has none,
  and the overdue colour it would otherwise borrow is a purple, which reads as "different" rather
  than "worse" on a grid.
*/
.risk-band--Low      { --tone: var(--color-status-passed); }
.risk-band--Medium   { --tone: #e4d03b; }
.risk-band--High     { --tone: var(--color-warning); }
.risk-band--Critical { --tone: var(--color-status-failed); }

.risk-band--Low, .risk-band--Medium, .risk-band--High, .risk-band--Critical {
  --tone-ink: color-mix(in oklab, var(--tone) 38%, var(--color-default-900));
}

.risk-score {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
  font-size: 0.8125rem;
}

.risk-score__chip {
  display: inline-grid;
  place-items: center;
  min-width: 1.9rem;
  height: 1.6rem;
  padding: 0 0.35rem;
  border-radius: 0.375rem;
  background: color-mix(in oklab, var(--tone) 22%, transparent);
  border: 1px solid color-mix(in oklab, var(--tone) 50%, transparent);
  color: var(--tone-ink);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.risk-score__band { color: var(--tone-ink); font-weight: 500; }

/* The health of a control a risk leans on: a dot and a word. */
.risk-health {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  white-space: nowrap;
  color: var(--tone-ink);
  font-size: 0.8125rem;
}

.risk-health::before {
  content: "";
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: var(--tone);
}

/*
  The 5×5 grid, as a picker in the editor and as a heat map on the dashboard. Likelihood runs up the
  side (5 at the top, as every risk matrix draws it) and impact along the bottom.
*/
.risk-matrix {
  display: grid;
  grid-template-columns: auto repeat(5, minmax(0, 1fr));
  gap: 3px;
  align-items: stretch;
}

.risk-matrix__axis {
  display: grid;
  place-items: center;
  font-size: 0.68rem;
  color: var(--color-default-500);
  padding: 0.15rem 0.35rem;
  text-align: center;
  line-height: 1.15;
}

.risk-matrix__cell {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 2.6rem;
  border-radius: 0.375rem;
  border: 1px solid color-mix(in oklab, var(--tone) 40%, transparent);
  background: color-mix(in oklab, var(--tone) 16%, transparent);
  color: var(--tone-ink);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background 0.12s ease, box-shadow 0.12s ease;
}

.risk-matrix__cell:hover { background: color-mix(in oklab, var(--tone) 30%, transparent); }

.risk-matrix__cell[aria-pressed="true"] {
  background: color-mix(in oklab, var(--tone) 55%, transparent);
  box-shadow: 0 0 0 2px var(--tone);
  color: var(--color-default-900);
}

/* A heat-map cell with risks in it is the thing to see, so it wears its colour strongly. */
.risk-matrix__cell[data-count]:not([data-count="0"]) {
  background: color-mix(in oklab, var(--tone) 42%, transparent);
  border-color: var(--tone);
  color: var(--color-default-900);
}

.risk-matrix__cell[data-count]:not([data-count="0"]):hover {
  background: color-mix(in oklab, var(--tone) 60%, transparent);
}

/* An empty heat-map cell is still coloured by where it is, but quietly. */
.risk-matrix__cell[data-count="0"] {
  background: color-mix(in oklab, var(--tone) 6%, transparent);
  color: color-mix(in oklab, var(--tone-ink) 45%, transparent);
  cursor: default;
}

.risk-matrix__cell[data-count="0"]:hover { background: color-mix(in oklab, var(--tone) 6%, transparent); }

.risk-matrix__cell:disabled { cursor: default; }

.risk-matrix__title {
  grid-column: 2 / -1;
  text-align: center;
  font-size: 0.68rem;
  line-height: 1.4;
  letter-spacing: 0.04em;
  color: var(--color-default-500);
  padding-top: 0.2rem;
}

.risk-matrix__side {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-default-500);
  text-align: center;
}

.risk-matrix-wrap {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.35rem;
  align-items: center;
}

/* A health bar on the risk dashboard wears its own tone rather than the lateness ramp. */
.dash-band__fill.risk-fill { background: var(--tone); }
