/* Extracted from index.html so the operator console can run under a
   Content-Security-Policy that forbids inline script. */
  :root {
    --ink: #e8edf4; --dim: #8fa0b5; --line: #22303f; --bg: #0b1119;
    --panel: #101a25; --brand: #38bdf8; --warn: #fbbf24; --bad: #f87171;
    --ok: #4ade80;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0; background: var(--bg); color: var(--ink);
    font: 400 15px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  }
  .wrap { max-width: 1180px; margin: 0 auto; padding: 0 20px; }
  header { border-bottom: 1px solid var(--line); padding: 14px 0; }
  /* Wraps rather than overflows. Three items on one line is 320px of content in
     a 288px space, and the thing that gives way when it does not fit has to be
     the layout, not the page: a header that pushes the document sideways takes
     every table on it out of the viewport too. */
  header .wrap { display: flex; align-items: center; gap: 8px 16px; flex-wrap: wrap; }
  .logo { font-weight: 650; letter-spacing: -.01em; }
  .logo span { color: var(--brand); }
  /* The wordmark is the page's h1 on all three screens, because each of them is
     the whole page while it is up. Sized as it always was: making it a heading
     changes what a screen reader finds, and should change nothing anyone sees. */
  h1.logo { font-size: 1em; margin: 0; }
  .login h1.logo { margin-bottom: 14px; }
  .who { margin-left: auto; color: var(--dim); font-size: 14px; }
  /* When the figures on this page were fetched. Beside the operator's name
     rather than carrying its own `margin-left: auto`: two auto margins in one
     flex row split the free space between them and push the two apart, which
     reads as two unrelated things instead of one line of context. */
  .stamp { color: var(--dim); font-size: 13px; }
  button {
    font: inherit; border-radius: 8px; border: 1px solid var(--line);
    background: var(--panel); color: var(--ink); padding: 7px 13px; cursor: pointer;
  }
  button:hover { border-color: var(--brand); }
  /* Every keyboard focus is visible, and the ring sits *outside* the control so
     that one rule can serve the filled primary button, whose background is this
     exact colour. 2px against #0b1119 is 8.9:1, and against the panel and the
     input fill it is higher still. `:focus-visible` rather than `:focus`, so a
     mouse click does not leave a ring behind on a button that was just pressed. */
  :focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
  button.primary { background: var(--brand); color: #06202f; border-color: transparent; font-weight: 600; }
  button.danger:hover { border-color: var(--bad); color: var(--bad); }
  button:disabled { opacity: .45; cursor: not-allowed; }
  input, select {
    font: inherit; background: #0a121b; color: var(--ink);
    border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; width: 100%;
  }
  label { display: block; font-size: 13px; color: var(--dim); margin: 0 0 4px; }
  .field { margin-bottom: 12px; }
  /* The five section headings under the console's own h1. Sized exactly as they
     were when each of them was an h1 in its own right, because the problem with
     five of those was the outline a screen reader builds from them, not the
     type: nothing here should look any different for having been fixed. */
  h2 { font-size: 21px; margin: 26px 0 4px; letter-spacing: -.02em; }
  .sub { color: var(--dim); margin: 0 0 18px; font-size: 14px; }
  /* Reachable by a screen reader, absent to everyone else. Used for the header
     of the actions column, which has nothing to write in it and would otherwise
     be an unnamed column. */
  .sr-only {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
  table { width: 100%; border-collapse: collapse; font-size: 14px; }
  th, td { text-align: left; padding: 9px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
  th { color: var(--dim); font-weight: 600; font-size: 12.5px; text-transform: uppercase; letter-spacing: .05em; }
  .pill { display: inline-block; padding: 2px 9px; border-radius: 999px; font-size: 12.5px; border: 1px solid var(--line); }
  .pill.trial { color: var(--brand); border-color: #1d4c66; }
  .pill.active { color: var(--ok); border-color: #1e4632; }
  .pill.past_due { color: var(--warn); border-color: #4d3d15; }
  .pill.suspended, .pill.churned, .pill.purged { color: var(--bad); border-color: #4d2020; }
  .card { border: 1px solid var(--line); border-radius: 12px; padding: 18px; background: var(--panel); }
  .row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  .muted { color: var(--dim); }
  /* Never `display: none`, for the reason `.modal-msg` at the foot of this file
     spells out and hit first: every one of these is a live region, and a box
     that leaves the accessibility tree and comes back is a box a screen reader
     has no reason to read out. So the box itself stays put, always, and what
     arrives with a tone is the padding, the colour and the contents. Empty and
     toneless it is a block of no height that costs nothing.

     The two standing banners hold a button as well as their words, which is why
     the children are hidden rather than the parent: a Retry button with nothing
     to retry has no business on the page, but the region it sits in does. */
  .msg { font-size: 14px; }
  .msg > * { display: none; }
  .msg.ok, .msg.err, .msg.warn { padding: 10px 13px; border-radius: 8px; margin: 14px 0; }
  .msg.ok > *, .msg.err > *, .msg.warn > * { display: revert; }
  .msg.err { background: #2a1416; color: #fca5a5; }
  .msg.ok { background: #10261a; color: #86efac; }
  /* Neither. Weak recovery codes are not a failure and not a success: they are a
     thing worth doing something about, and painting that red would put it in the
     same colour as a request that did not work. */
  .msg.warn { background: #29200f; color: #fcd34d; }
  /* A message that offers a fix keeps the button on the same line as the
     problem, so the two are read as one thing rather than as a stray control. */
  .msg button { margin-left: 10px; padding: 4px 10px; font-size: 13px; }
  /* The page's own answer box follows the reader down.

     It sits first inside <main>, and every row action reports into it. With
     fourteen clinics the last row is about 2,100px below it, so suspending the
     practice at the foot of the list wrote the confirmation off the top of the
     screen and then refreshed the table underneath: the row changed and the
     sentence explaining it was never seen, including the sentence saying why a
     refusal was refused.

     Sticky rather than a floating toast, because it is still the same element in
     the same place in the document: nothing moves for a screen reader, the live
     region is untouched, and when the page is already at the top it looks
     exactly as it did. Only this one box, not `.msg` generally: the two standing
     banners belong to the top of the page and pinning them would cost a line of
     every screen for as long as they are up. */
  #msg.ok, #msg.err, #msg.warn {
    position: sticky; top: 10px; z-index: 30;
    box-shadow: 0 6px 20px rgba(2, 6, 12, .55);
  }
  /* Checksums and identifiers, which have to be readable character by character
     and have to wrap rather than push the card wider than the page. */
  code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; word-break: break-all; }
  /* Recovery codes: shown once, and meant to be transcribed or copied whole.
     Light on dark so they are the brightest thing on the page, monospaced and
     spaced out so a long run of characters can be read across without losing
     the line, and `user-select: all` so one click takes the set rather than one
     code. Used by enrolment and by rotation, which show the same thing. */
  .codes {
    background: #f8fafc; color: #0f172a; padding: 12px; border-radius: 8px;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 14px; line-height: 1.7; letter-spacing: .04em; margin: 0 0 12px;
    user-select: all; white-space: pre-wrap; word-break: break-all;
  }
  /* `width` rather than `max-width`, with the gutter as padding on the outside
     of it, so the card cannot end up flush against the edge of a phone: at 390px
     it had about five pixels each side, which reads as a rendering fault rather
     than as a margin. Below 380px the card takes what there is, minus the same
     gutter. */
  .login { width: min(380px, 100%); padding: 0 16px; margin: 12vh auto; }
  .hide { display: none; }
  .actions { display: flex; gap: 6px; flex-wrap: wrap; }
  .log { font-size: 13.5px; }
  /* Frozen billing periods, inside the usage dialog. Four columns in a dialog on
     a phone would push the whole thing sideways, so the wrapper scrolls and the
     table stays a table. Not `display: block` on the table itself: that is the
     usual trick for this and it drops the table's own semantics in some
     browsers, which is a screen reader losing the row and column relationships
     to buy a scrollbar. Focusable for the same reason the log tables are, a
     region only a mouse can scroll is one a keyboard cannot read the end of. */
  .periods-wrap { overflow-x: auto; }
  /* Drawn inside the box, unlike the global rule at the top: this one can be as
     wide as the dialog and an outward ring would be clipped by its edge. */
  .periods-wrap:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
  .periods { font-size: 13.5px; white-space: nowrap; }
  .periods td, .periods th { padding: 7px 10px; }
  /* Six columns and five columns do not fit a phone, so they scroll sideways in
     here rather than pushing the whole document out. Focusable, because a region
     that only a mouse can scroll is one a keyboard cannot read the end of;
     index.html carries the tabindex and the name that goes with it. The ring is
     drawn inside the box, unlike everywhere else, because this one is as wide as
     the page and an outward ring would be clipped by the viewport. */
  .table-scroll { overflow-x: auto; }
  .table-scroll:focus-visible { outline-offset: -2px; }
  /* The clinic list stops being a scroll box once there is room for its four
     columns, and that is what lets its header stay on screen.

     `overflow-x: auto` makes an element a scroll container on *both* axes, which
     means a sticky `th` inside one sticks to the top of the box rather than to
     the top of the window, in a box with no height limit, which is to say it
     never sticks at all. Above 900px the four columns fit inside the 1180px
     page with room to spare, so the wrapper stops clipping and the header
     behaves. Below it the wrapper scrolls exactly as before and the header
     scrolls with it, which is the right trade at a width where the whole list is
     four or five rows tall anyway.

     `tabindex` on that wrapper is set by ops.js from whether it actually
     overflows, so this does not leave a tab stop on a box that cannot scroll. */
  @media (min-width: 900px) {
    #clinicsRegion { overflow: visible; }
    /* On the row, not the cells: `border-collapse: collapse` gives the border to
       the table, so a sticky `th` leaves its bottom rule behind. The background
       is opaque for the same reason it is needed at all. */
    .clinics thead th {
      position: sticky; top: 0; z-index: 2;
      background: var(--bg); box-shadow: inset 0 -1px 0 var(--line);
    }
  }
  /* What a table says when there is nothing in it, nothing yet, or nothing that
     would load. Centred and dim so it reads as the state of the table rather
     than as a row of data with most of its cells missing. */
  .placeholder td { color: var(--dim); text-align: center; padding: 22px 10px; }
  /* The console had no links at all until the clinic hostname became one, and a
     browser's own link colour on this background is close to unreadable. */
  a { color: var(--brand); }

  /* ── Needs attention ──────────────────────────────────────────────────────
     Deliberately plain. It is the first thing on the page and it is read in a
     hurry, so it is a list of sentences, not a dashboard: no counters, no
     sparklines, nothing that needs a legend. */
  .warnings { list-style: none; margin: 0; padding: 0; }
  .warnings > li {
    border: 1px solid var(--line); border-left-width: 3px; border-radius: 10px;
    background: var(--panel); padding: 12px 14px; margin: 0 0 10px;
  }
  /* The urgency is on the edge of the card as well as in the pill, because the
     edge is what is read when the page is scanned rather than looked at. */
  .warnings > li.now { border-left-color: var(--bad); }
  .warnings > li.soon { border-left-color: var(--warn); }
  .warn-head {
    margin: 0; display: flex; align-items: baseline; gap: 6px 10px; flex-wrap: wrap;
  }
  .warn-head a { font-weight: 600; }
  /* Every reason, in its own sentence, saying what is true and why that is
     enough to be on this list. One clinic can be here for more than one. */
  .warn-why { margin: 6px 0 0; padding-left: 18px; color: var(--dim); font-size: 14px; }
  .warn-why li { margin: 2px 0; }
  /* An empty note is still a paragraph with a margin, and this one is empty
     whenever every rule has a field to read. */
  #attentionGaps:empty { display: none; }

  /* Reusing the pill, in the two levels the rules produce. Named for the window
     rather than for a severity word, because "now" and "this week" are what an
     operator is deciding between. */
  .pill.now { color: var(--bad); border-color: #4d2020; }
  .pill.soon { color: var(--warn); border-color: #4d3d15; }

  /* A warning inside a block of prose: the SES and Stripe checks, and the
     uninvoiced periods in the usage dialog. Six call sites wrote `class="warn"`
     and nothing ever defined it, so every one of them rendered in body ink,
     indistinguishable from the sentence around it. The colour is the same
     `--warn` the pills and the standing banner already use, so the three agree.

     A `<p class="warn">` earns a panel, an inline `<span>` earns the colour and
     the weight only: a tinted block around two words in the middle of a table
     cell reads as a rendering fault. */
  .warn { color: var(--warn); font-weight: 600; }
  p.warn {
    background: #29200f; border-radius: 8px; padding: 10px 13px; margin: 12px 0;
    font-weight: 400;
  }
  p.warn strong { color: #fde68a; }

  /* The filter over the clinic list. One line where there is room for one. */
  .search { display: flex; align-items: flex-end; gap: 8px; margin: 0 0 14px; }
  /* Narrower than the text box beside it, because a status name is a known short
     string and a practice name is not. `flex: 0 0 auto` so it keeps its width
     when the search field grows. */
  .search .field.narrow { flex: 0 0 auto; }
  .search .field.narrow select { width: auto; min-width: 170px; }
  /* `.hide` alone would lose to the `display: flex` above: same weight, earlier
     in the file. Exactly the trap `.modal-wrap.hide` at the foot of this file
     hit first, and it showed up here as a search box on a deployment with six
     clinics, which is the one thing the box is meant not to be. */
  .search.hide { display: none; }
  .search .field { margin: 0; flex: 1 1 260px; }
  @media (max-width: 480px) { .search { flex-wrap: wrap; } }

  /* The row a warning above points at, for as long as it is the target. Without
     it a jump into a list of near-identical rows lands the reader somewhere they
     have to work out. */
  .clinics tr:target { outline: 2px solid var(--brand); outline-offset: -2px; }
  /* The rows carry tabindex="-1" so that a warning's link moves focus and not
     only the scroll position. That makes them focusable without making them
     controls, so a row that has focus for some other reason should not look like
     one, and the :target rule above is what actually marks the arrival.
     `:not(:target)` rather than a bare `:focus`, because following one of those
     links makes a row both at once: the two selectors weigh the same, this one
     is later in the file, and without the exclusion it wins and takes away the
     only mark saying which row was jumped to. */
  .clinics tr:focus:not(:target) { outline: none; }

  /* The slug, under the name it belongs to.

     It was on screen only as the middle of a hostname, and it is the string the
     import card insists be typed exactly, the string every audit row is keyed
     by, and the string the confirmation dialog asks to have retyped before a
     practice goes offline. Monospaced because it is an identifier and is read
     character by character, and selectable whole so copying it takes one
     gesture rather than a careful drag. */
  .slug {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 12.5px; color: var(--dim); user-select: all;
  }

  /* Copying something the console will never show again: the TOTP setup key, a
     set of recovery codes, a first administrator's password. Small, quiet, and
     beside the thing it copies rather than under it. */
  .copy { padding: 3px 9px; font-size: 12.5px; margin-left: 8px; vertical-align: middle; }

  /* Whether an audited action worked, in the same pill the rest of the console
     uses for state. `asked` is the third answer and not a failure: the log pairs
     `export_requested` with a later `export_completed` or `export_failed`, so
     the request row's own outcome is genuinely not yet known from itself. */
  .pill.ok { color: var(--ok); border-color: #1e4632; }
  .pill.bad { color: var(--bad); border-color: #4d2020; }
  .pill.asked { color: var(--dim); }

  /* The audit filters. Wraps rather than overflows: four controls and two
     buttons do not fit a phone on one line, and the thing that gives way has to
     be the layout. */
  .filters { display: flex; align-items: flex-end; gap: 8px; flex-wrap: wrap; margin: 0 0 14px; }
  .filters .field { margin: 0; }
  .filters .field input, .filters .field select { width: auto; min-width: 150px; }
  /* Beneath the log, holding the one control that pages it. */
  .more { margin: 12px 0 0; }

  /* Every date the API returns for one clinic, a keystroke away from the row and
     out of the way until then. Native, so it needs no script and no ARIA. */
  .facts { margin-top: 8px; }
  .facts summary { color: var(--dim); font-size: 13px; cursor: pointer; }
  .facts dl {
    margin: 8px 0 0; display: grid; grid-template-columns: auto 1fr; gap: 2px 12px;
    font-size: 13px;
  }
  .facts dt { color: var(--dim); }
  .facts dd { margin: 0; }

  /* Export and Suspend, set apart from looking at a practice.
     Opening their hostname and reading their dates are ordinary; copying every
     record they hold onto this laptop, or taking them offline, are not, and a
     row that puts all four side by side invites the fifth click of a scan to be
     one of the last two. So they sit in their own bordered group, and Restore
     stays outside it: that one ends an outage.

     `inline-flex` so the box is the size of what is in it. As a flex item in the
     wide layout that changes nothing, but at the width where the cell around it
     is a block it stops the box stretching the whole card and reading as a
     region of the row rather than as two buttons that go together.

     This comment was closed twice: once after "ends an outage" and again at the
     end. Everything between the two became the start of a selector, CSS dropped
     the rule it swallowed, and the box below has never rendered in any browser
     since it was written. `test_ops_console_assets.py` now fails on an unbalanced
     comment marker, because prose explaining a rule is not evidence the rule
     applies. */
  .risky {
    display: inline-flex; gap: 6px; flex-wrap: wrap;
    border: 1px solid #3a2429; border-radius: 8px; padding: 5px; background: #1a1216;
  }

  /* The one dialog. It is the only thing on this page that has to cover the
     console, so it is the only thing here with a z-index. */
  .modal-wrap {
    position: fixed; inset: 0; z-index: 50;
    display: flex; overflow-y: auto; padding: 20px;
  }
  /* `.hide` alone would lose to the `display: flex` above: same weight, later in
     the file. Said here rather than left to depend on rule order. */
  .modal-wrap.hide { display: none; }
  .modal-back { position: absolute; inset: 0; background: rgba(2, 6, 12, .74); }
  /* `margin: auto` rather than centring the flex container, so a dialog taller
     than the window scrolls with its top still reachable instead of being
     clipped off the screen. */
  .modal {
    position: relative; margin: auto; width: 100%; max-width: 460px;
    border: 1px solid var(--line); border-radius: 12px; background: var(--panel);
    padding: 20px; box-shadow: 0 24px 64px rgba(0, 0, 0, .6);
  }
  /* It carries tabindex="-1" to catch clicks on its own prose. That makes it
     focusable without making it a control, so it should not look like one. */
  .modal:focus { outline: none; }
  .modal h2 { font-size: 18px; margin: 0 0 6px; letter-spacing: -.015em; }
  .modal .sub { margin: 0 0 14px; }
  /* An action with nothing to explain leaves the description blank, and an empty
     paragraph still carries its margin. */
  .modal .sub:empty { display: none; }
  .modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
  /* The committing button of a destructive action reads as one. The table's
     Suspend uses the same word for the same reason. */
  .modal-actions button.danger {
    background: #3a1519; border-color: #5c2127; color: #fca5a5; font-weight: 600;
  }
  .modal-actions button.danger:hover { border-color: var(--bad); }
  /* Never `display: none`, even with nothing in it. This is the dialog's live
     region, and a box that leaves the accessibility tree and comes back is one a
     screen reader has no reason to announce. Empty, it takes no room. */
  .modal-msg:not(:empty) {
    margin-top: 14px; padding: 10px 13px; border-radius: 8px; font-size: 14px;
    background: #16212e; color: var(--ink);
  }
  .modal-msg.err:not(:empty) { background: #2a1416; color: #fca5a5; }
  .modal-msg.ok:not(:empty) { background: #10261a; color: #86efac; }

  @media (max-width: 760px) { .row { grid-template-columns: 1fr; } }

  /* One card per clinic instead of six columns squeezed into a phone. The
     header row goes, and each cell carries the column name it lost as generated
     content, so a status still says what it is a status of.

     `display: block` on a row or a cell drops its implicit table role in more
     than one browser, which is why index.html writes `role="table"`,
     `role="row"` and `role="cell"` out by hand: they are a no-op at every width
     where this rule does not apply, and they are the whole of the semantics at
     the widths where it does. */
  @media (max-width: 640px) {
    .clinics, .clinics tbody, .clinics tr, .clinics td { display: block; }
    .clinics thead { display: none; }
    .clinics tr {
      border: 1px solid var(--line); border-radius: 12px; background: var(--panel);
      padding: 12px 14px; margin-bottom: 10px;
    }
    .clinics td { border: 0; padding: 4px 0; }
    .clinics td::before {
      content: attr(data-label); display: block; color: var(--dim);
      font-size: 12.5px; text-transform: uppercase; letter-spacing: .05em;
    }
    /* The practice names itself, and the buttons say what they do. A label over
       either would be a word of chrome above a word of content. */
    .clinics td:first-child::before, .clinics td.actions::before { content: none; }
    .clinics td.actions { padding-top: 10px; }
    /* The cell is a block at this width rather than the flex row it is above it,
       so the group that holds Export and Suspend needs its own gap from the
       Restore button that can sit in front of it. */
    .clinics td.actions .risky { margin-top: 6px; }
    /* A table saying it is empty is not six cards of nothing. */
    .clinics tr.placeholder { background: none; border-style: dashed; }
    .clinics .placeholder td { padding: 18px 0; }
    .clinics .placeholder td::before { content: none; }
  }

  /* A 320px phone has 40px of it in gutters at the padding above, and the
     tables are the things that pay for it. */
  @media (max-width: 480px) { .wrap { padding: 0 16px; } }
