/* One hand-written sheet, no build and no third party: what is committed is what is served.
   Single column at every width, because the view that has to work is an approver deciding on a
   phone.

   Two faces, split by what has to be read exactly. Mono is for the strings somebody compares
   character by character or copies into a terminal: the account key, the permission set, the
   device code, the request id. Everything else is sans, including the status word, the clock and
   the page's own name, which were identifiers only by association and made the whole page read
   as a terminal dump. Both stacks are the platform's, since the page may fetch nothing from a
   third party. */

:root {
  --sans: system-ui, -apple-system, "Segoe UI", sans-serif;
  /* Named faces ahead of `ui-monospace`, which resolves to SF Mono on a Mac: SF Mono is drawn for
     an editor and is tight and narrow at the 0.75rem this page sets a request id in. Menlo has the
     wider advance and the larger x-height, and every face below it is a near relative, so the line
     reads the same width wherever it lands. All of them ship with the operating system; the page
     may fetch no font, and this keeps it fetching none. */
  --mono:
    Menlo, "Cascadia Mono", Consolas, "DejaVu Sans Mono", "Liberation Mono", ui-monospace, monospace;

  --ink: #14181f;
  --muted: #5b6674;
  --page: #f6f7f9;
  --card: #ffffff;
  --line: #d9dee5;
  --hair: #eceff3;
  --lift: 0 1px 1px rgb(20 24 31 / 4%), 0 2px 8px rgb(20 24 31 / 5%);
  /* Reserved for one meaning: press this. The primary action, the focus ring, and nothing else. */
  --accent: #1f5fd0;
  --on-accent: #ffffff;
  /* Waiting on a person: the accent, on purpose. A fourth hue was tried here and every candidate
     cost more than it gave. Amber and gold read as a warning about the request rather than a
     state of it; violet and teal read as a second blue at small sizes; a neutral left the tab
     badge with nothing to say. Blue already means "this concerns a person" on this page, and
     what a pending row is is a decision somebody still owes.

     Kept as its own name and not spelled `--accent` at each site, because that is the whole cost
     of changing the answer later: one line here. The other three say the rest, and each has one
     job: green is access that is live, red is a refusal or a grant that fell over, amber is the
     panel on the sign-in page telling you to check a code before you act. */
  --wait: var(--accent);
  --wait-mark: var(--accent);
  --on-wait: var(--on-accent);
  --open: #2e7d32;
  --warn-ink: #6b3c00;
  --warn-page: #fff6e5;
  --warn-line: #e8c98a;
  --bad: #b3261e;
}

@media (prefers-color-scheme: dark) {
  :root {
    --ink: #e7eaef;
    --muted: #9aa5b3;
    --page: #14181f;
    --card: #1c222b;
    --line: #2e3742;
    --hair: #252d38;
    --lift: 0 1px 1px rgb(0 0 0 / 30%), 0 2px 8px rgb(0 0 0 / 35%);
    --accent: #6ea2ff;
    /* Dark ink on the lighter accent: white on it is about 2.6:1, which is a button label
       nobody can read. */
    --on-accent: #14181f;
    --open: #57b85c;
    --warn-ink: #f0d9ac;
    --warn-page: #2e2513;
    --warn-line: #5d4a22;
    --bad: #ff8a80;
  }

  /* The one asset on the page that cannot take its colour from a token. */
  select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1.5 1.75 6 6.25 10.5 1.75' fill='none' stroke='%239aa5b3' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  }
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0;
  /* A column as tall as the window, so the footer sits on the bottom edge of a short page rather
     than halfway up it: the sign-in screen is one button and a warning, and left the rule
     floating in the middle of the viewport. `dvh` second, so a browser that has it uses it and
     the phone's collapsing address bar stops shifting the footer. */
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
  background: var(--page);
  color: var(--ink);
  font: 16px/1.5 var(--sans);
}

.shell,
main {
  margin: 0 auto;
  /* Explicit, because `margin: 0 auto` on a flex child would otherwise shrink it to its content
     and centre that instead of holding the measure. */
  width: 100%;
  max-width: 44rem;
  padding: 1rem;
}

/* Takes whatever the window has left over, which is the whole of what pins the footer down. The
   room above the heading grows with it: at one rem the page opened hard against the header rule,
   which on a large screen reads as a page that started before you were ready. */
main {
  flex: 1 0 auto;
  padding: 1.35rem 1rem 2rem;
}

@media (min-width: 48rem) {
  main {
    padding: 2.5rem 1rem 3.5rem;
  }

  h1 {
    font-size: 1.5rem;
  }

  .lede {
    font-size: 1.05rem;
  }
}

/* The bands are the width of the window and the shell inside them keeps main's measure: a rule
   that stopped where the text stops read as an unfinished edge rather than as the page's own.
   Card over page, so the two are a shade lighter than what they frame in both schemes. */
.band {
  background: var(--card);
}

.band.top {
  border-bottom: 1px solid var(--line);
}

.band.bottom {
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.8rem;
}

.band.top .shell {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 0.5rem 1rem;
  padding-bottom: 0;
}

.brand {
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: -0.01em;
  color: inherit;
  text-decoration: none;
}

.session {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0;
  margin-left: auto;
  line-height: 1.35;
  text-align: right;
  color: var(--muted);
  font-size: 0.8rem;
}

.session .holding {
  font-family: var(--mono);
  font-size: 0.85em;
  opacity: 0.85;
}

/* On a phone the right-aligned stack was three ragged lines pulling against a brand and a nav
   that both start at the left margin. One wrapped row under the brand instead: the same three
   strings, shorter, and reading down the same edge as everything else. */
@media (max-width: 34rem) {
  .session {
    flex-basis: 100%;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 0.6rem;
    margin-left: 0;
    text-align: left;
  }
}

/* Wraps rather than scrolls. `overflow-x: auto` computes `overflow-y` to `auto` too, which made
   the nav a scroll box exactly as tall as its links and clipped 4px off the top and bottom of the
   focus ring on all three tabs, at every width. There are three tabs; they can have a second row
   on a narrow phone. */
nav {
  display: flex;
  flex-basis: 100%;
  flex-wrap: wrap;
  gap: 0 1.25rem;
}

nav a {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0;
  border-bottom: 2px solid transparent;
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
}

/* A tab that says how much is behind it, so the page you are not looking at can still tell you
   there is something on it. */
.pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.3rem;
  height: 1.3rem;
  padding: 0 0.4rem;
  border-radius: 0.65rem;
  font-size: 0.7rem;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* Something is waiting for a person, which is the one thing the accent is kept for. */
.pill.act {
  background: var(--wait-mark);
  color: var(--on-wait);
}

/* Yours, waiting on somebody else: worth knowing, and not a call to act. */
.pill.mine {
  background: var(--hair);
  color: var(--muted);
}

nav a:hover {
  color: var(--ink);
}

nav a.here {
  border-bottom-color: var(--ink);
  color: var(--ink);
}

/* Underlined rather than coloured, so the accent is left saying one thing. */
a {
  color: inherit;
  text-decoration-color: var(--line);
  text-underline-offset: 0.2em;
}

a:hover {
  text-decoration-color: currentcolor;
}

h1 {
  margin: 0 0 0.35rem;
  font-size: 1.25rem;
  line-height: 1.25;
  letter-spacing: -0.015em;
}

p {
  margin: 0 0 0.75rem;
}

.lede {
  max-width: 34rem;
  color: var(--muted);
}

/* The way out of the one-request view. That view is what a Slack notification opens, so it can be
   the first page somebody sees, and until now the only way on from it was the browser's own back
   button. */
.back {
  display: inline-block;
  margin-bottom: 0.6rem;
  color: var(--muted);
  font-size: 0.85rem;
}

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

.hint {
  color: var(--muted);
  font-size: 0.85rem;
}

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

.notice:empty {
  display: none;
}

.notice {
  margin: 0.75rem 0;
}

/* The send went through. One sentence, in ink and a weight above the small print, and then the
   card under it: the request as it now exists, which is a better receipt than a sentence naming
   an id. */
.sent {
  margin: 0;
  font-weight: 600;
}

.outcome:empty {
  display: none;
}

.outcome {
  margin-top: 0.5rem;
}

.outcome .hint {
  margin: 0.5rem 0 0;
}

/* The receipt: what was asked for, the id it was given, and what happens next. A panel, because
   loose lines under a button read as more form. */
.sent-box {
  padding: 0.7rem 0.85rem;
  border: 1px solid var(--hair);
  border-radius: 0.5rem;
  background: var(--card);
  box-shadow: var(--lift);
}

.sent-ask {
  margin: 0 0 0.15rem;
  font-family: var(--mono);
  font-size: 0.9rem;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.sent-box .hint {
  margin: 0.4rem 0 0;
}

label {
  display: block;
  margin: 0.9rem 0 0.25rem;
  font-weight: 600;
  font-size: 0.9rem;
}

input,
select,
textarea {
  width: 100%;
  min-height: 2.75rem;
  padding: 0.6rem;
  border: 1px solid var(--line);
  border-radius: 0.4rem;
  background: var(--card);
  color: inherit;
  font: inherit;
}

/* An account key and a permission set name are identifiers, read exactly, so they keep the mono
   face the card's own headline sets them in: you pick the string you will later be shown. A size
   down from body, because at 16px a 22-character identifier filled the control and read as the
   loudest thing on a form whose loudest thing should be the reason.

   The arrow is ours, drawn once as a data URI, which `img-src 'self' data:` admits and which
   needs no file of its own. Left to the platform it is the one part of the form that arrives
   looking like it came from somewhere else. */
select {
  appearance: none;
  padding-right: 2.4rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1.5 1.75 6 6.25 10.5 1.75' fill='none' stroke='%235b6674' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.9rem center;
  background-size: 12px 8px;
  font-family: var(--mono);
  font-size: 0.9rem;
  line-height: 1.3;
}

select:hover:not(:disabled) {
  border-color: var(--muted);
}

/* The hours picker is the interface's own words and not an identifier, so it keeps the sans the
   labels are set in. */
select.duration {
  font-family: var(--sans);
}

/* The same argument while a control rests on its placeholder: "Choose an account" is the page
   speaking, and it goes back to mono the moment it holds a real key. */
select:has(option[value=""]:checked) {
  font-family: var(--sans);
}

/* Which cell of the catalog, and for how long, is one decision, so it is one line read left to
   right. Stacked, the three choices read as three unrelated questions. One column under the
   breakpoint, because a phone cannot hold two identifiers side by side and an approver on one is
   the view that has to work. */
.ask {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0.35rem 0.75rem;
}

/* Everything that is not part of that row spans it: the hint under it, the hours, the reason, the
   counter, the button and whatever the send came back with. */
.ask > * {
  grid-column: 1 / -1;
}

.field {
  margin-top: 0.65rem;
}

/* The field's own label, and not every label inside it. Unscoped, this hung 0.35rem under each of
   the twelve hour blocks, which sized the track's row taller than the blocks and left a bare
   strip of it showing under them. */
.field > label {
  margin: 0 0 0.35rem;
}

.ask .hint,
.ask .notice {
  margin: 0;
}

.ask .hint:empty {
  display: none;
}

.ask .button {
  margin: 0.35rem 0 0;
}

@media (min-width: 40rem) {
  /* The permission set takes the wider share: it is the longest identifier on the page, and the
     account list is short and stable. Hours left this row and has one of its own. */
  .ask {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr);
  }

  .ask > .pick {
    grid-column: auto;
  }

  /* Natural width once there is room for it. Full width on a phone, where it is a thumb target
     at the bottom of the form. */
  .ask .button {
    justify-self: start;
  }
}

/* Hours: a row of its own, laid out the way a card reports the same number. The figure on the
   left at a fixed width, the twelve-hour ruler to the right of it, so the control that sets the
   hours and the bar that reports them are one shape read twice. */
.hours-field {
  margin-top: 0.85rem;
}

.hours-label {
  flex: none;
  margin: 0;
  font-weight: 600;
  font-size: 0.9rem;
}

.hours-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

/* Fixed, so the ruler starts in the same place whatever the figure says: sized to its own text
   the whole track shifted under the thumb every time the number gained a digit. */
.hours-out {
  flex: none;
  width: 4.25rem;
  font-size: 0.85rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.hours-range {
  flex: 1 1 auto;
  min-height: 2.25rem;
  padding: 0;
  border: 0;
  background: none;
  accent-color: var(--accent);
}

/* At the cell's ceiling: amber, not the error red, because the maximum is an allowed ask and red
   here is kept for refusals. */
.hours-range.max {
  accent-color: var(--warn-ink);
}

/* One track cut into twelve, not twelve boxes: gapped outlined squares with a number in each are
   a rating scale, and what this sets is a length. Six to a row under the breakpoint, where twelve
   would be slivers. The live ones say how long this cell allows, the dead ones out of how much. */
.hours-blocks {
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  border: 1px solid var(--line);
  border-radius: 0.4rem;
  background: var(--card);
}

/* The radio is off screen but still focusable and still what the arrow keys move between; the
   block beside it is what is seen, and `for` is what makes it operate the radio. */
.hour-input {
  position: absolute;
  width: 1px;
  height: 1px;
  min-height: 0;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.hour {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 2.25rem;
  margin: 0;
  border-left: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  user-select: none;
}

/* Hairlines inside one outline, and a corner only where the track itself has one. */
.hour:nth-of-type(6n + 1) {
  border-left: 0;
}

.hour:nth-of-type(n + 7) {
  border-top: 1px solid var(--line);
}

.hour:first-of-type {
  border-top-left-radius: 0.35rem;
}

.hour:nth-of-type(6) {
  border-top-right-radius: 0.35rem;
}

.hour:nth-of-type(7) {
  border-bottom-left-radius: 0.35rem;
}

.hour:last-of-type {
  border-bottom-right-radius: 0.35rem;
}

.hour:hover {
  background: var(--hair);
}

/* Filled up to what was asked for, in the one colour this page keeps for something live, and the
   chosen hour solid at the head of the fill: a length with a handle on it rather than twelve
   equal choices. */
.hour.on {
  background: color-mix(in srgb, var(--accent) 16%, var(--card));
  color: var(--ink);
}

.hour-input:checked + .hour {
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
}

/* Past this cell's ceiling, or before an account has named one. Still counted and still legible,
   plainly not yours to press. */
.hour-input:disabled + .hour {
  background: var(--hair);
  color: color-mix(in srgb, var(--muted) 55%, var(--hair));
  cursor: not-allowed;
}

/* Lifted, because the ring sits outside the block and the neighbour's fill is painted after it. */
.hour-input:focus-visible + .hour {
  position: relative;
  z-index: 1;
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Twelve to a row once there is width for it: the shape the card draws its bar in. */
@media (min-width: 30rem) {
  .hours-blocks {
    grid-template-columns: repeat(12, minmax(0, 1fr));
  }

  .hour:nth-of-type(n + 7) {
    border-top: 0;
  }

  .hour:nth-of-type(7) {
    border-left: 1px solid var(--line);
    border-bottom-left-radius: 0;
  }

  .hour:nth-of-type(6) {
    border-top-right-radius: 0;
  }

  .hour:first-of-type {
    border-bottom-left-radius: 0.35rem;
  }

  .hour:last-of-type {
    border-top-right-radius: 0.35rem;
  }
}

/* Nothing to pick yet. The field holds its place, disabled: hiding it moved the reason box up the
   page at the moment somebody was reaching for it. */
.hours-field.dead .hours-label,
.hours-field.dead .hours-out {
  color: var(--muted);
}

/* Every rule below sets `display`, which outranks the user agent's own `[hidden]` rule: without
   this the attribute silently does nothing and a hidden pager stays on screen. */
[hidden] {
  display: none !important;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Tapped on a phone by an approver who was pinged in Slack, so never under the 44px target. */
  min-height: 2.75rem;
  margin: 0.25rem 0;
  padding: 0.5rem 1.1rem;
  border: 1px solid var(--line);
  border-radius: 0.4rem;
  background: var(--card);
  color: inherit;
  font: inherit;
  text-decoration: none;
  cursor: pointer;
}

.button.primary {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
}

/* Worn only by the confirm step of a refusal, never by the resting Deny: the weight belongs to
   the press that commits, not to the one that opens the question. `--card` for the label, the
   same flip `--on-accent` makes, so it reads in both schemes. */
.button.danger {
  border-color: var(--bad);
  background: var(--bad);
  color: var(--card);
  font-weight: 600;
}

/* Muted rather than faded: 0.6 opacity on an already-muted label drops under 4.5:1 exactly while
   a decision is in flight and somebody is watching for an answer. */
.button[disabled] {
  border-color: var(--line);
  background: var(--hair);
  color: var(--muted);
  cursor: progress;
}

/* The ask form disables its button for "not valid yet", not "in flight", so no spinner there. */
.ask .button[disabled] {
  cursor: not-allowed;
}

button.link {
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 0.8rem;
  text-decoration: underline;
  text-decoration-color: var(--line);
  text-underline-offset: 0.2em;
  cursor: pointer;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 0.2rem;
}

.warning {
  margin: 1.25rem 0;
  padding: 0.9rem 1rem;
  border: 1px solid var(--warn-line);
  border-radius: 0.5rem;
  background: var(--warn-page);
  color: var(--warn-ink);
}

.warning p:last-child {
  margin-bottom: 0;
  font-size: 0.9rem;
}

/* The one loud thing on the page. A device code is read off this screen and typed into another,
   so it is set as cells rather than as a word: the unit a person copies is the character. */
/* The code and the way out sit on one line: they are one instruction, and stacking them put the
   button below the fold on a phone. Wraps rather than shrinks, so the code stays readable. */
.device-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1rem;
  margin: 1rem 0;
}

.code {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}

.code .cell {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 1.9em;
  padding: 0.2rem 0;
  border: 1px solid var(--line);
  border-bottom-width: 3px;
  border-radius: 0.3rem;
  background: var(--card);
  font-family: var(--mono);
  font-size: clamp(1.1rem, 6vw, 1.5rem);
  font-weight: 600;
  line-height: 1.1;
}

.code .gap {
  align-self: center;
  width: 0.6rem;
  height: 2px;
  background: var(--line);
}

.groups {
  display: flex;
  flex-direction: column;
}

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

/* The boundary the count in the page heading could not draw: it said two of these need you
   without saying which two. More room above than below, so a heading belongs to the group under
   it rather than floating between two. */
/* A rule across the column, not a bigger word: the card's own headline is 1rem and a group that
   competed with it on size would make the page two headings deep with nothing leading. Set in
   the same case as everything else, because the rule under it is what makes it a divider, and
   letterspaced capitals on top of that were a second way of saying divider. */
.group {
  display: flex;
  align-items: baseline;
  gap: 0.45rem;
  margin: 1.75rem 0 0.6rem;
  padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.85rem;
  font-weight: 600;
}

.groups > .group:first-child {
  margin-top: 0.25rem;
}

/* The one place the accent still says what it was reserved to say, now that the per-card pending
   token is a word in the fact line: something here is waiting for a person. */
.group.waiting {
  color: var(--wait);
}

/* At the far end of the rule, where a count belongs: the heading says what, this says how many,
   and nothing sits between them to be read as part of the name. */
.group-count {
  margin-left: auto;
  color: var(--muted);
  font-weight: 400;
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
}

/* One hairline the whole way round. The 4px coloured edge was a second encoding of a status the
   mark and the word already carry, and with the rows grouped under their own headings it was a
   third saying the same thing. */
.card {
  padding: 0.75rem 0.9rem;
  /* A hairline and not a rule: with the lift under it the card is already off the page, and the
     darker outline made every row read as a framed box. */
  border: 1px solid var(--hair);
  border-radius: 0.5rem;
  background: var(--card);
  box-shadow: var(--lift);
}

.card p {
  margin: 0;
}

/* The cell on the left, how long on the right, on one baseline. They wrap onto separate rows on a
   narrow phone rather than squeezing the identifier. */
.card .head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.1rem 1rem;
}

/* The line the decision turns on: which permission set, on which account. It leads the card, in
   the exact face, and it is the only thing on it set above body size. */
.card .cell {
  flex: 1 1 15rem;
  font-family: var(--mono);
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}

/* Everything else the system knows, run together as small print. As four stacked blocks these
   competed with the decision. */
.card .meta {
  margin-top: 0.3rem;
  color: var(--muted);
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

/* First in the fact line: a drawn mark and the word it stands for, both in the status colour.
   The mark is `aria-hidden`, and the word beside it is what a screen reader reads, which is why
   it is a word and never an emoji. */
.card .token {
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
}

.state-icon {
  fill: none;
  stroke: currentcolor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  vertical-align: -0.18em;
  margin-right: 0.15rem;
}

/* The one sentence on the card a person wrote, and the one thing on it that must be read whole:
   deliberately not clamped, because approving privileged access without the reason in front of
   you is not a decision. */
.card .why {
  margin: 0.55rem 0 0;
  padding-left: 0.7rem;
  border-left: 2px solid var(--line);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Copied into a terminal or compared against one, so it stays in the exact face and stays whole.
   Directly under the line it names: at the end of the action row it sat where a reader looks for
   what they can do, and it is not something to do. */
.id {
  display: inline-block;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.7rem;
  overflow-wrap: anywhere;
}

.card .id {
  margin-top: 0.15rem;
}

/* A fixed set of slots, so nothing moves as a card changes state: whatever can be decided at the
   start, Revoke always at the far end, the receipt on its own row. Revoke used to land wherever
   the buttons before it happened to stop, which on four kinds of card was four different
   places. */
.card .actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem 0.6rem;
  margin-top: 0.5rem;
}

.card .actions .revoke {
  margin-left: auto;
}

.card .actions .hint {
  font-size: 0.8rem;
}

/* Compact, because these sit in a card's small print and not at the end of a form: Approve, Deny
   and Revoke at a form button's size were the loudest thing on a listing. 36px clears the 24px
   minimum target with room, and a pointer that cannot hit it gets the full 44 back below. */
.card .actions .button {
  min-height: 2.25rem;
  margin-top: 0;
  margin-bottom: 0;
  padding: 0.3rem 0.8rem;
  border-radius: 0.35rem;
  font-size: 0.85rem;
}

@media (pointer: coarse) {
  .card .actions .button {
    min-height: 2.75rem;
    padding: 0.5rem 1.05rem;
    font-size: 0.9rem;
  }

  .hour {
    min-height: 2.75rem;
  }

  .drawer {
    min-height: 2.75rem;
  }
}

/* A decision pair, swapped in place for its confirm step. */
.card .decide {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

/* What you just did, on the card you did it on, under the thumb that did it. On its own row, so
   it is never squeezed between two buttons. */
.card .receipt {
  order: 1;
  flex-basis: 100%;
  margin: 0.1rem 0 0;
  /* Ink and a heavier weight, because every other small line on the card is muted fact and this
     one is the answer to something the reader just did. */
  color: var(--ink);
  font-size: 0.8rem;
  font-weight: 600;
}

.card .receipt:empty {
  display: none;
}

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

/* Focused only after a decision, never by tabbing. The ring says where the list put you back. */
.card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The window, to scale against twelve hours: length is how long, fill is how much of it is
   spent. Comparable between cards only because the scale is fixed (cards.js, SCALE_HOURS).
   Short and beside its own figure: given the full width it read as the most important thing on
   the card, and it carries one number. */
.window-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
}

.window {
  flex: none;
  width: 5.25rem;
  height: 0.55rem;
}

/* Cut with the card's own background rather than drawn in a colour of their own: the bar becomes
   the same twelve blocks the hours were asked for in, without a second line weight to read. */
.window .tick {
  stroke: var(--card);
  stroke-width: 1;
}

.window .track {
  fill: var(--hair);
}

.window .span {
  fill: var(--line);
}

.window .spent {
  fill: var(--muted);
}

.window-hours {
  font-size: 0.8rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* Every status a request can hold, so a card's rule is never a status nobody assigned a colour.
   `ended` and `expired` keep the neutral default deliberately: both mean the window is over and
   there is nothing left to do about them. */

.card.status-pending .token,
.card.status-pending .window-hours {
  color: var(--wait);
}

.card.status-pending .span {
  fill: color-mix(in srgb, var(--wait-mark) 22%, transparent);
}

.card.status-approved .token,
.card.status-granted .token {
  color: var(--open);
}

.card.status-approved .span,
.card.status-granted .span {
  fill: color-mix(in srgb, var(--open) 22%, transparent);
}

.card.status-granted .spent {
  fill: var(--open);
}

.card.status-denied .token,
.card.status-error .token {
  color: var(--bad);
}

/* The history behind a heading you press, at the foot of the list. As a checkbox above the cards
   it asked a question before the reader had seen anything; what it opens is a section, so it is
   set like the group headings it stands among and carries its count the same way. */
.drawer {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  width: 100%;
  min-height: 2.25rem;
  margin: 1.75rem 0 0.6rem;
  padding: 0.4rem 0;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}

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

.drawer .caret {
  flex: none;
  transition: transform 0.15s ease;
}

/* Down while it is closed, up while it is open: the mark says which way the press goes. */
.drawer[aria-expanded="true"] .caret {
  transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
  .drawer .caret {
    transition: none;
  }
}

/* The count and the way to more of it, on one line under the groups. */
.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin-top: 1.25rem;
}

.pager .tally {
  margin: 0;
}

.pager .tally:empty {
  display: none;
}

.fallback {
  margin-top: 2rem;
  color: var(--muted);
}

.fallback summary {
  padding: 0.35rem 0;
  cursor: pointer;
}

.fallback textarea {
  font-family: var(--mono);
  font-size: 0.85rem;
}

.band.bottom .shell {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.25rem 1.5rem;
}

.brand img {
  border-radius: 4px;
  vertical-align: -0.3em;
}

.colophon {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0;
}

.legal {
  margin: 0;
}

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