/* Keera Gateway control panel.
   One stylesheet, no build step. Tokens first, then layout, then components. */

:root {
  /* Native parts the tokens cannot reach, like an open list of options and
     scrollbars, follow this. */
  color-scheme: light;
  --bg: #f7f7f5;
  --surface: #ffffff;
  --surface-2: #fafaf9;
  --surface-3: #f1f0ed;
  --border: #e4e2dd;
  --border-strong: #d2cfc8;
  --text: #17171a;
  --muted: #6b6a66;
  --faint: #93918b;
  --accent: #2d4ea0;
  --accent-soft: #eaeffb;
  --accent-fg: #ffffff;
  --good: #1c7a4d;
  --good-soft: #e6f4ec;
  --warn: #9a5b00;
  --warn-soft: #fbf0dd;
  --bad: #a92a20;
  --bad-soft: #fbeae8;
  --s1: #2d4ea0;
  --s2: #1c7a4d;
  --s3: #9a5b00;
  --s4: #7b3fa0;
  --s5: #0e6d7c;
  --s6: #a92a20;
  --shadow: 0 1px 2px rgb(23 23 26 / 6%), 0 8px 24px -12px rgb(23 23 26 / 18%);
  --radius: 10px;
  --radius-sm: 7px;
  /* Uniform bump for the whole panel; viewport-height rules below divide by it. */
  --ui-scale: 1.1;
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
  --sans:
    -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto,
    "Helvetica Neue", Arial, sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --bg: #121215;
    --surface: #1a1a1e;
    --surface-2: #1f1f24;
    --surface-3: #26262c;
    --border: #2b2b32;
    --border-strong: #3a3a43;
    --text: #ececef;
    --muted: #9c9ba4;
    --faint: #74737c;
    --accent: #849dee;
    --accent-soft: #1e2438;
    --accent-fg: #10131c;
    --good: #57cb90;
    --good-soft: #16281f;
    --warn: #dfa445;
    --warn-soft: #2a2113;
    --bad: #ef7b70;
    --bad-soft: #2c1917;
    --s1: #849dee;
    --s2: #57cb90;
    --s3: #dfa445;
    --s4: #c193da;
    --s5: #58c2d2;
    --s6: #ef7b70;
    --shadow: 0 1px 2px rgb(0 0 0 / 40%), 0 10px 30px -14px rgb(0 0 0 / 70%);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #121215;
  --surface: #1a1a1e;
  --surface-2: #1f1f24;
  --surface-3: #26262c;
  --border: #2b2b32;
  --border-strong: #3a3a43;
  --text: #ececef;
  --muted: #9c9ba4;
  --faint: #74737c;
  --accent: #849dee;
  --accent-soft: #1e2438;
  --accent-fg: #10131c;
  --good: #57cb90;
  --good-soft: #16281f;
  --warn: #dfa445;
  --warn-soft: #2a2113;
  --bad: #ef7b70;
  --bad-soft: #2c1917;
  --s1: #849dee;
  --s2: #57cb90;
  --s3: #dfa445;
  --s4: #c193da;
  --s5: #58c2d2;
  --s6: #ef7b70;
  --shadow: 0 1px 2px rgb(0 0 0 / 40%), 0 10px 30px -14px rgb(0 0 0 / 70%);
}

* {
  box-sizing: border-box;
}

/* The components below set their own display, which outranks the browser's
   rule for [hidden] and leaves a "hidden" button on screen. */
[hidden] {
  display: none !important;
}

html {
  zoom: var(--ui-scale);
}
html,
body {
  height: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.5 var(--sans);
  -webkit-font-smoothing: antialiased;
  font-variant-numeric: tabular-nums;
}

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

h1,
h2,
h3 {
  margin: 0;
  font-weight: 600;
  letter-spacing: -0.011em;
}
h1 {
  font-size: 19px;
}
h2 {
  font-size: 15px;
}
h3 {
  font-size: 13px;
}

code,
.mono {
  font-family: var(--mono);
  font-size: 0.92em;
}

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

/* ---------------------------------------------------------------- boot */

.boot {
  display: grid;
  place-items: center;
  height: 100%;
}

.boot-mark {
  width: 26px;
  height: 26px;
  border: 2.5px solid var(--border-strong);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .boot-mark {
    animation-duration: 2s;
  }
  * {
    transition: none !important;
  }
}

/* -------------------------------------------------------------- shell */

/* The shell is exactly the viewport, not at least the viewport. A definite
   height is what lets a pane inside it overflow and scroll on its own; with
   min-height the row grows to fit its content instead, and a long playground
   conversation stretches the page rather than scrolling. */
.shell {
  display: grid;
  grid-template-columns: 232px 1fr;
  height: calc(100vh / var(--ui-scale));
  transition: grid-template-columns 0.14s ease;
}

.sidebar {
  background: var(--surface);
  border-right: 1px solid var(--border);
  padding: 18px 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  position: sticky;
  top: 0;
  height: calc(100vh / var(--ui-scale));
  overflow-y: auto;
}

/* The name and the mark are the way back to Overview, so the brand is a link
   that answers to the pointer the way the entries below it do. It is still the
   product's name at the top of its own panel, so it keeps the text colour and
   gains no underline. The space before the first group is a margin rather than
   padding, to keep the hover surface an even pill around the name. */
.brand {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 5px 10px;
  margin-bottom: 13px;
  border-radius: var(--radius-sm);
  font-weight: 620;
  letter-spacing: -0.02em;
  font-size: 15px;
  color: inherit;
  transition:
    background 0.12s,
    color 0.12s;
}

.brand:hover {
  background: var(--surface-3);
  text-decoration: none;
}

/* The mark is the favicon file itself, sized by the width and height the
   caller gives it, so there is one drawing of it in the deployment. */
.brand-mark {
  display: block;
  border-radius: 6px;
  flex: none;
}

/* The hamburger in the topbar and the scrim behind the drawer are the narrow
   layout's, and nothing else ever draws them: the media query at the end of
   this file is what brings them out. The toggle is a .btn, and .btn comes
   later in the file, so both rules name the two classes to win over it. */
.btn.nav-toggle,
.nav-scrim {
  display: none;
}

/* Each labelled block of the sidebar. The sidebar's own gap now falls between
   groups, so the group re-establishes it between its own items. */
.nav-group {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.nav-label {
  padding: 14px 10px 6px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faint);
}

.nav-item {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  color: var(--muted);
  font-weight: 500;
  cursor: pointer;
  border: 0;
  background: none;
  font: inherit;
  width: 100%;
  text-align: left;
  transition:
    background 0.12s,
    color 0.12s;
}
.nav-item:hover {
  background: var(--surface-3);
  color: var(--text);
  text-decoration: none;
}
.nav-item[aria-current="page"] {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}
.nav-item svg {
  width: 16px;
  height: 16px;
  flex: none;
}

/* The entry that reveals the screens this deployment does not use. Quieter
   than a real destination, because it is an offer rather than a place. */
.nav-more {
  color: var(--faint);
  font-weight: 500;
}
.nav-more .nav-text {
  font-size: 12.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.nav-collapse {
  color: var(--faint);
}
.nav-collapse svg {
  transition: transform 0.14s ease;
}

.sidebar-foot {
  margin-top: auto;
  padding-top: 16px;
}

.account {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid var(--border);
}
.account-id {
  min-width: 0;
  flex: 1;
}
.account-email {
  font-size: 12.5px;
  font-weight: 550;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.account-role {
  font-size: 11px;
  color: var(--muted);
  text-transform: capitalize;
}

.main {
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.topbar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 28px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  position: sticky;
  top: 0;
  z-index: 20;
}
.topbar .spacer {
  flex: 1;
}
.page-sub {
  color: var(--muted);
  font-size: 12.5px;
  margin-top: 2px;
}

/* The scroll container for every view: the frame around it - sidebar and
   topbar - stays put, and a view taller than the pane scrolls here. That is
   also what gives the playground a definite height to fill, so its transcript
   scrolls instead of growing the page. Every other view sits at the top of it
   and looks exactly as it did. */
.content {
  padding: 24px 28px 64px;
  width: 100%;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

/* ------------------------------------------------------------ controls */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 13px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-weight: 550;
  font-size: 13px;
  cursor: pointer;
  white-space: nowrap;
  transition:
    background 0.12s,
    border-color 0.12s,
    opacity 0.12s;
}
.btn:hover:not(:disabled) {
  background: var(--surface-3);
  /* A link can look like a button, and links underline on hover. */
  text-decoration: none;
}
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-fg);
}
.btn-primary:hover:not(:disabled) {
  background: var(--accent);
  filter: brightness(1.08);
}
.btn-danger {
  color: var(--bad);
  border-color: var(--border-strong);
}
.btn-danger:hover:not(:disabled) {
  background: var(--bad-soft);
  border-color: var(--bad);
}
.btn-quiet {
  border-color: transparent;
  background: none;
  color: var(--muted);
}
.btn-quiet:hover:not(:disabled) {
  background: var(--surface-3);
  color: var(--text);
}
.btn-sm {
  padding: 4px 9px;
  font-size: 12px;
}
/* A button that is a switch rather than an action, outside a segmented control.
   So far that is the request log's Live. */
.btn[aria-pressed="true"] {
  border-color: var(--accent);
  color: var(--accent);
}
.btn svg {
  width: 15px;
  height: 15px;
}

.input,
.select {
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  width: 100%;
  min-width: 0;
}
.input:focus,
.select:focus {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
  border-color: var(--accent);
}
.input::placeholder {
  color: var(--faint);
}
/* The browser's own arrow differs per platform and ignores the theme, so the
   select draws its own. */
.select {
  cursor: pointer;
  appearance: none;
  padding-right: 30px;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position:
    calc(100% - 16px) 50%,
    calc(100% - 11px) 50%;
  background-size: 5px 5px;
  background-repeat: no-repeat;
}

/* Where the browser lets CSS style the open list too, it looks like the rest
   of the panel instead of the operating system's menu. */
@supports (appearance: base-select) {
  .select,
  .select::picker(select) {
    appearance: base-select;
  }
  /* The arrow drawn above stays; this one is heavier and points sideways
     while open. */
  .select::picker-icon {
    display: none;
  }
  .select::picker(select) {
    margin-block: 4px;
    padding: 4px;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
    box-shadow: var(--shadow);
  }
  .select option {
    padding: 6px 8px;
    border-radius: 5px;
    font-size: 13px;
    cursor: pointer;
  }
  .select option:hover,
  .select option:focus-visible {
    background: var(--surface-3);
    outline: none;
  }
  .select option:checked {
    color: var(--accent);
  }
  .select option::checkmark {
    color: var(--accent);
  }
  .select option:disabled {
    color: var(--faint);
    cursor: not-allowed;
  }
}

/* A field whose value cannot be set here.
   Being disabled is the whole explanation the form gives, so it has to look
   disabled rather than only refuse the keystroke. */
.input:disabled,
.select:disabled {
  background-color: var(--surface-2);
  border-color: var(--border);
  color: var(--muted);
  -webkit-text-fill-color: var(--muted);
  opacity: 1;
  cursor: not-allowed;
}
.select:disabled {
  cursor: not-allowed;
}
input[type="checkbox"]:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}
.field:has(> .input:disabled) > label,
.field:has(> .select:disabled) > label,
.field label.row-tight:has(> input:disabled) {
  color: var(--muted);
}

.field {
  display: grid;
  gap: 5px;
  /* Each row takes its content's height and no more. Inside a .field-row the
     two fields are grid items of the same height, and rows that stretched
     would hand the difference to the control: two inputs at two different
     heights, side by side, because one hint ran onto a second line. */
  align-content: start;
  margin-bottom: 14px;
}
.field label {
  font-size: 12.5px;
  font-weight: 550;
}

/* A band across a long form, naming the part of the object below it. The rule
   above it is what does the work: it is the only thing that tells a reader
   the eight fields are four pairs rather than eight settings. */
.field-section {
  margin: 4px 0 12px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.field-section:first-of-type {
  border-top: 0;
  padding-top: 0;
}
.field-section h3 {
  margin: 0;
  font-size: 12px;
  font-weight: 650;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
}
.field-section p {
  margin: 3px 0 0;
  font-size: 11.5px;
  color: var(--faint);
  line-height: 1.45;
}
/* A footnote: under a field, and under a screen that has something to say
   about what it just showed. It was scoped to .field, which left the
   standalone ones - the ones under a report - rendering as body text. */
.hint {
  font-size: 11.5px;
  color: var(--muted);
  line-height: 1.45;
}
.field-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  /* Both fields start at the top of the row, so the one with less to say
     underneath is not stretched to the height of the one with more. */
  align-items: start;
}
/* Where a filled-in field's value came from. It is a fact about the value
   rather than help with the field, so it sits a shade behind the hint. */
.field .hint-origin {
  font-size: 11.5px;
  color: var(--faint);
  line-height: 1.45;
}

/* A fold in a form: what a hosted provider filled in, which is a group of
   answers rather than a group of questions. It starts closed - see
   hostedFields. */
.fold {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  padding: 10px 12px;
  margin-bottom: 14px;
}
.fold > summary {
  cursor: pointer;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--muted);
}
.fold[open] > summary {
  margin-bottom: 12px;
}
.fold > :last-child {
  margin-bottom: 0;
}
.fold .field:last-child,
.fold .field-row:last-child .field {
  margin-bottom: 0;
}

.seg {
  display: inline-flex;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface);
}
.seg button {
  border: 0;
  background: none;
  font: inherit;
  font-size: 12.5px;
  font-weight: 500;
  padding: 5px 11px;
  cursor: pointer;
  color: var(--muted);
  border-right: 1px solid var(--border);
}
.seg button:last-child {
  border-right: 0;
}
.seg button[aria-pressed="true"] {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}

/* --------------------------------------------------------------- tiles */

/* A choice taken before the form it configures, rather than a select inside
   one. It is here for the filter and router modes, where the fields below have
   almost nothing in common from one mode to the next: a dialog that asks which
   mode last is a dialog that is mostly hidden until it is answered. It is here
   for a model's provider too, where the tile carries the provider's own logo.
   Tiles also have room for the sentence an <option> had to leave out. */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(178px, 1fr));
  /* Every row as tall as the tallest, so a set that wraps does not leave the
     last row shorter than the one above it. */
  grid-auto-rows: 1fr;
  gap: 8px;
}
.tile {
  display: grid;
  gap: 3px;
  justify-items: start;
  text-align: left;
  padding: 11px 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  cursor: pointer;
}
.tile:hover {
  border-color: var(--accent);
  background: var(--surface-2);
}
.tile[aria-pressed="true"] {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.tile-mark {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  margin-bottom: 3px;
  border-radius: var(--radius-sm);
  background: var(--surface-3);
  color: var(--accent);
}
/* On the chosen tile the mark's own panel would be a second wash of the accent
   over the first; the surface it sits on carries the state instead. */
.tile[aria-pressed="true"] .tile-mark {
  background: var(--surface);
}
.tile-mark svg {
  width: 16px;
  height: 16px;
}
/* A provider's own logo on a tile. It is artwork rather than an icon drawn to
   the same 24-square as the others, so it is given the box and keeps its own
   proportions inside it - a wide wordmark is not squared off to fit. */
.tile-mark .provider-mark {
  width: 18px;
  height: 18px;
}
.tile-name {
  font-size: 13px;
  font-weight: 600;
}
.tile-what {
  font-size: 11.5px;
  color: var(--muted);
  line-height: 1.4;
}

/* The same choice after it is made: one row, and the way back to the others. */
.tile-chosen {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.tile-chosen > svg {
  width: 17px;
  height: 17px;
  flex: none;
  color: var(--accent);
}
.tile-chosen .spacer {
  flex: 1;
}

/* --------------------------------------------------------------- cards */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.card-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--border);
}
.card-head .spacer {
  flex: 1;
}
.card-body {
  padding: 16px;
}

.grid {
  display: grid;
  gap: 16px;
}
/* A page whose sections are laid out by .grid gets its rhythm from the gap, so
   the header must not also carry its own margin - the two would add up and put
   twice as much air under the header as between everything else. */
.grid > .detail-head {
  margin-bottom: 0;
}
.grid-2 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-3 {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-4 {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.stat {
  padding: 14px 16px;
}
.stat-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--faint);
}
.stat-value {
  font-size: 24px;
  font-weight: 620;
  letter-spacing: -0.02em;
  margin-top: 6px;
  line-height: 1.15;
}
.stat-value .unit {
  font-size: 14px;
  font-weight: 500;
  color: var(--muted);
  margin-left: 3px;
}
.stat-note {
  font-size: 12px;
  color: var(--muted);
  margin-top: 4px;
}

/* A row's name, which is also the way in to that row's own screen. It reads as
   the name it already was until it is pointed at. */
.row-link {
  color: var(--text);
  font-weight: 600;
}
.row-link:hover {
  color: var(--accent);
}

/* -------------------------------------------------------------- detail */

/* One team, one key, one model. Everything above the chart: the way back to
   the list, what this thing is, and the window being read. */
.detail-head {
  display: grid;
  gap: 10px;
  margin-bottom: 16px;
}

/* The way back, which is also the only thing on a detail screen that says
   which list it came from - the page title is the entity's own name. */
.crumb {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  color: var(--muted);
  width: fit-content;
}
.crumb:hover {
  color: var(--text);
  text-decoration: none;
}
.crumb svg {
  width: 14px;
  height: 14px;
}

/* A heading for a section that is not inside a card, because the card under it
   is a table and a table's own head is its column names. */
.section-head {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 0 0 12px;
}

/* A row the request log received while it was on the screen. It fades from a
   wash to nothing over a few seconds rather than staying marked: what it
   answers is "which line is new", which stops being a question almost at once,
   and a table that accumulates highlights is a table nobody can scan. The bar
   down the left edge is what survives a colour nobody can see. */
tr.row-new > td {
  animation: row-new 4s ease-out forwards;
}
tr.row-new > td:first-child {
  animation-name: row-new-edge;
}
@keyframes row-new {
  from {
    background: var(--accent-soft);
  }
  to {
    background: transparent;
  }
}
@keyframes row-new-edge {
  from {
    background: var(--accent-soft);
    box-shadow: inset 2px 0 0 var(--accent);
  }
  to {
    background: transparent;
    box-shadow: inset 2px 0 0 transparent;
  }
}

/* A row for something that has finished: a sandbox that has been deleted, has
   expired or has failed. Held back rather than hidden, because the whole
   reason those rows are on the screen at all is that somebody asked to see
   what a sandbox cost after it was gone. */
.row-faint td {
  opacity: 0.55;
}

/* Label and value, for the facts about a thing that are not measurements. */
.facts {
  display: grid;
  gap: 11px;
}
.fact {
  display: grid;
  grid-template-columns: minmax(0, 110px) minmax(0, 1fr);
  gap: 12px;
  align-items: baseline;
}

/* --------------------------------------------------------------- table */

.table-wrap {
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}
th {
  text-align: left;
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--faint);
  padding: 9px 16px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
td {
  padding: 11px 16px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
tbody tr:last-child td {
  border-bottom: 0;
}
td.num,
th.num {
  text-align: right;
}
td.shrink,
th.shrink {
  width: 1%;
  white-space: nowrap;
}

/* A sortable header is a real button: reachable from the keyboard, and it says
   what it does on hover rather than waiting to be discovered by a click. */
th .th-sort {
  border: 0;
  background: none;
  padding: 0;
  margin: 0;
  cursor: pointer;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
th .th-sort:hover {
  color: var(--text);
}
th[aria-sort] .th-sort {
  color: var(--text);
}
/* Right-aligned columns sort from the other side, so the arrow stays next to
   the number rather than out at the edge of the column. */
th.num .th-sort {
  flex-direction: row-reverse;
}
.sort-arrow {
  font-size: 10px;
  line-height: 1;
}

/* What a search or a toggle left. It replaces the rows rather than sitting
   under them, because an empty table with a filled-in search box above it
   reads as a list that has lost its contents. */
.table-none {
  padding: 30px 16px;
  text-align: center;
  color: var(--muted);
}

/* The controls above a list that grows: the search box, the toggles that
   narrow it, and how much of it is being looked at. */
.toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}
.toolbar-search {
  width: auto;
  flex: 0 1 260px;
}
.toolbar-select {
  width: auto;
}
.toolbar label {
  font-size: 12.5px;
  color: var(--muted);
  cursor: pointer;
}

.empty {
  padding: 40px 16px;
  text-align: center;
  color: var(--muted);
}
.empty strong {
  display: block;
  color: var(--text);
  margin-bottom: 4px;
  font-weight: 600;
}

/* --------------------------------------------------------------- bits */

.pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 11.5px;
  font-weight: 550;
  background: var(--surface-3);
  color: var(--muted);
  white-space: nowrap;
}
.pill-good {
  background: var(--good-soft);
  color: var(--good);
}
.pill-warn {
  background: var(--warn-soft);
  color: var(--warn);
}
.pill-bad {
  background: var(--bad-soft);
  color: var(--bad);
}
.pill-accent {
  background: var(--accent-soft);
  color: var(--accent);
}

/* A hosted provider's own mark, beside the name it belongs to. The marks have
   different proportions, so what is held equal between them is their height,
   at the size of the text they sit with; the width follows from the artwork. */
.provider-mark {
  height: 1em;
  width: auto;
  flex: none;
}
/* A logo about twice as wide as it is tall looks too big at full height next
   to the square ones. A tile's fixed square already fits it. */
.provider-mark-wide {
  height: 0.7em;
}

.dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

/* A pill that is also the way in to the detail behind it. */
.pill-button {
  border: 0;
  cursor: pointer;
  font-family: inherit;
}
.pill-button:hover {
  filter: brightness(1.06);
}
a.pill-button:hover {
  text-decoration: none;
}

/* The ceiling a guardrail field is already held inside, said next to the field
   rather than in a paragraph above it. */
.ceiling {
  color: var(--accent);
}

/* A standing instruction, shown to somebody who is subject to it rather than
   writing it. It is prose an administrator wrote into this reader's own
   requests, so it keeps the line breaks they typed and sits in a block of its
   own instead of running into the copy around it. */
.prompt-text {
  white-space: pre-wrap;
  background: var(--surface-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 9px 11px;
  font-size: 12.5px;
  line-height: 1.5;
}

/* An instruction in a table cell. The whole of it belongs on the screen that
   edits it; here it is the two lines that say which filter this row is. */
.clamp-2 {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  font-size: 12px;
}

/* Waiting for something that is not a chat token: a model check. */
.blip {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  animation: blink 1.1s ease-in-out infinite;
}

/* The same dot standing for a connection rather than for a wait, on the request
   log's Live switch. It says which of three things is true, because they are
   three different situations for whoever is reading an unchanging screen: it is
   watching, they turned it off, or it is not connected and the quiet afternoon
   they think they are looking at is a stream that stopped. */
.blip[data-state="live"] {
  animation: none;
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.blip[data-state="connecting"] {
  background: var(--warn);
}
.blip[data-state="paused"] {
  background: var(--faint);
  animation: none;
}
.blip[data-state="stopped"] {
  background: var(--bad);
  animation: none;
}

.meter {
  height: 5px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
  min-width: 90px;
}
.meter-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 999px;
}
.meter-fill.warn {
  background: var(--warn);
}
.meter-fill.bad {
  background: var(--bad);
}

/* The request waterfall: where one request's time went.
   ------------------------------------------------------------------------
   A row per step, its bar placed on a track that spans the request's whole
   latency. The track is shared by every row, so the gaps between bars are as
   readable as the bars - a step that started late is the thing being looked
   for, and a chart that closed its gaps would hide it. */
.flame {
  display: grid;
  grid-template-columns: minmax(120px, max-content) 1fr max-content;
  gap: 3px 10px;
  align-items: center;
}
.flame-row {
  display: contents;
}
.flame-name {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-size: 12px;
  white-space: nowrap;
}
.flame-name .faint {
  font-size: 11px;
}
.flame-track {
  position: relative;
  height: 12px;
  border-radius: 3px;
  background: var(--surface-3);
  min-width: 80px;
}
.flame-bar {
  position: absolute;
  top: 0;
  bottom: 0;
  min-width: 2px;
  border-radius: 3px;
  background: var(--accent);
}
.flame-took {
  font-size: 11.5px;
  color: var(--muted);
  text-align: right;
  white-space: nowrap;
}

/* The four things a request's time can go on. They are colours rather than
   labels on the bars because the summary above the chart already names them,
   and a reader who has read it once reads the rest of the chart by colour. */
.flame-bar.client,
.flame-swatch.client {
  background: var(--faint);
}
.flame-bar.gateway,
.flame-swatch.gateway {
  background: var(--accent);
}
.flame-bar.guardrails,
.flame-swatch.guardrails {
  background: var(--s3);
}
.flame-bar.model,
.flame-swatch.model {
  background: var(--s5);
}
.flame-bar.bad,
.flame-swatch.bad {
  background: var(--bad);
}

.flame-legend {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 14px;
  font-size: 11.5px;
}
.flame-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}
.flame-key {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.flame-swatch {
  width: 9px;
  height: 9px;
  border-radius: 2px;
  flex: none;
}

@media (max-width: 560px) {
  /* The names wrap onto their own line rather than squeezing the tracks to
     nothing: the bars are the chart, and a track 20px wide is not one. */
  .flame {
    grid-template-columns: 1fr max-content;
  }
  .flame-name {
    grid-column: 1 / -1;
    white-space: normal;
  }
}

.muted {
  color: var(--muted);
}
.faint {
  color: var(--faint);
}
.nowrap {
  white-space: nowrap;
}
.row {
  display: flex;
  align-items: center;
  gap: 8px;
}
.row-tight {
  display: flex;
  align-items: center;
  gap: 5px;
}
.stack {
  display: grid;
  gap: 3px;
}
.wrap-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.key-value {
  font-family: var(--mono);
  font-size: 12.5px;
  background: var(--surface-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 11px 12px;
  word-break: break-all;
  user-select: all;
}

/* --------------------------------------------------------------- chart */

.chart {
  width: 100%;
  display: block;
}
.chart .grid-line {
  stroke: var(--border);
  stroke-width: 1;
}
.chart .axis-text {
  fill: var(--faint);
  font-size: 10px;
}
.chart .area {
  fill: var(--accent);
  opacity: 0.11;
}
.chart .line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.75;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.chart .err-bar {
  fill: var(--bad);
  opacity: 0.75;
}
.chart .hit {
  fill: transparent;
  cursor: crosshair;
}
.chart .hit:hover {
  fill: var(--text);
  opacity: 0.04;
}
.chart .cursor-line {
  stroke: var(--border-strong);
  stroke-width: 1;
  stroke-dasharray: 3 3;
}

.chart-tip {
  position: absolute;
  pointer-events: none;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  padding: 8px 10px;
  font-size: 12px;
  white-space: nowrap;
  z-index: 5;
}
.chart-tip .t {
  font-weight: 600;
  margin-bottom: 3px;
}
.chart-holder {
  position: relative;
}

.bars {
  display: grid;
  gap: 9px;
}
.bar-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 10px;
  align-items: center;
}
.bar-track {
  height: 22px;
  background: var(--surface-3);
  border-radius: var(--radius-sm);
  overflow: hidden;
  position: relative;
}
.bar-fill {
  height: 100%;
  border-radius: var(--radius-sm);
  opacity: 0.85;
}
.bar-label {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  padding: 0 9px;
  font-size: 12px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* -------------------------------------------------------------- session */

/* One task's calls, in the order the agent made them. It is not a chart of
   time: the bars are evenly spaced and each one is a turn, because what this
   answers is "where in the task did it go wrong and what did each turn cost",
   and a time axis over a task with a four-minute pause in it spends most of its
   width on the pause.

   It scrolls rather than squeezing. A task that made four hundred calls would
   otherwise be four hundred sub-pixel bars, which is a texture rather than a
   reading. */
.turns {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 64px;
  overflow-x: auto;
  padding-bottom: 2px;
}
.turn {
  flex: 1 0 auto;
  min-width: 5px;
  border-radius: 2px 2px 0 0;
  background: var(--accent);
  opacity: 0.75;
  cursor: pointer;
  border: 0;
  padding: 0;
}
.turn:hover,
.turn:focus-visible {
  opacity: 1;
}
.turn[data-tone="warn"] {
  background: var(--warn);
}
.turn[data-tone="bad"] {
  background: var(--bad);
}
/* A turn that cost nothing still has to be visible: a refusal is the row
   somebody is looking for, and a bar of no height is a row they cannot click. */
.turn[data-empty] {
  opacity: 0.5;
}

/* ---------------------------------------------------------- playground */

/* The transcript scrolls, not the page. A composer that walks off the bottom of
   a long conversation is the one control that always has to be reachable.

   The pane the chat sits in becomes a flex column for it, and the chat takes
   the whole of it. The generous bottom padding every other view is laid out
   with goes away here: it is meant to keep the last row clear of the viewport
   edge, and the composer is already pinned above that edge. Left on, it would
   push the chat past the pane and leave the pane itself scrollable - two
   nested scrollbars for one conversation. */
.content:has(> .chat) {
  display: flex;
  flex-direction: column;
  padding-bottom: 24px;
}

.chat {
  display: flex;
  flex-direction: column;
  gap: 12px;
  flex: 1;
  /* A floor, so the transcript does not collapse to a sliver - but a low one.
     The composer has to stay above the fold in a short window, and every pixel
     this floor claims past the pane's height is a pixel the composer sits
     below it. Under about 400px of viewport there is no arrangement that
     serves both, and the pane scrolls as a last resort. */
  min-height: 240px;
}

.chat-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}
.chat-settings {
  flex: none;
}

.chat-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.thread {
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: 20px;
}

.chat-empty {
  display: grid;
  gap: 6px;
  justify-items: center;
  text-align: center;
  padding: 56px 24px;
  max-width: 460px;
  margin: auto;
}
.chat-empty-mark {
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent);
  margin-bottom: 6px;
}
.chat-empty-mark svg {
  width: 19px;
  height: 19px;
}

.msg {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}
.msg-user {
  align-items: flex-end;
}
.msg-user .bubble {
  max-width: min(680px, 88%);
  padding: 9px 13px;
  border-radius: var(--radius);
  border-bottom-right-radius: 3px;
  background: var(--accent-soft);
}
.msg-user .bubble-unsent {
  background: var(--surface-3);
  color: var(--muted);
}

.msg-head {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 24px;
}
.msg-model {
  font-size: 11.5px;
  color: var(--muted);
}

.msg-body {
  display: grid;
  gap: 10px;
  min-width: 0;
}

/* A reply is plain text: nothing a model generates is ever handed to the
   browser as markup. Line breaks are honoured, because that is how prose,
   lists and unfenced code all arrive. */
.msg-text,
.msg-live {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.msg-live::after {
  content: "";
  display: inline-block;
  width: 7px;
  height: 14px;
  margin-left: 2px;
  vertical-align: text-bottom;
  background: var(--accent);
  opacity: 0.7;
  animation: blink 1s steps(2, start) infinite;
}
@keyframes blink {
  50% {
    opacity: 0;
  }
}

.msg-waiting {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--muted);
  font-size: 12.5px;
}
.msg-waiting .blip {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  animation: blink 1.1s ease-in-out infinite;
}

.msg-reasoning {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  font-size: 12.5px;
}
.msg-reasoning summary {
  padding: 6px 11px;
  cursor: pointer;
  color: var(--muted);
  font-weight: 550;
}
.msg-reasoning-body {
  padding: 0 11px 9px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  color: var(--muted);
}

.msg-foot {
  font-size: 11.5px;
  color: var(--faint);
}

.codeblock {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  overflow: hidden;
}
.codeblock-lang {
  padding: 4px 11px;
  border-bottom: 1px solid var(--border);
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
}
.codeblock pre {
  margin: 0;
  padding: 11px;
  overflow-x: auto;
}
.codeblock code {
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.55;
}

.composer-wrap {
  flex: none;
  display: grid;
  gap: 6px;
}
.composer {
  display: flex;
  align-items: flex-end;
  gap: 8px;
}
/* min-height holds one clean line before anything is typed; autosize takes
   over from there and stops at max-height. */
.composer-input {
  resize: none;
  overflow-y: auto;
  min-height: 38px;
  max-height: 260px;
  line-height: 1.5;
}
.composer-send {
  flex: none;
  height: 36px;
  padding: 0 13px;
}
.composer-hint {
  font-size: 11.5px;
  color: var(--faint);
}
.composer-tray {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.composer-thumb {
  position: relative;
  width: 64px;
  height: 64px;
}
.composer-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.composer-thumb-remove {
  position: absolute;
  top: -6px;
  right: -6px;
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--surface);
  color: var(--muted);
  cursor: pointer;
}
.composer-thumb-remove svg {
  width: 12px;
  height: 12px;
}
.bubble-images {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  /* Not stretched to the row's height, which would squash a wide image. */
  align-items: flex-start;
  gap: 6px;
}
/* Space only when text follows the images. */
.bubble-images:not(:last-child) {
  margin-bottom: 6px;
}
.bubble-images img {
  max-width: min(240px, 100%);
  max-height: 160px;
  border-radius: var(--radius-sm);
}

@media (prefers-reduced-motion: reduce) {
  .msg-live::after,
  .blip {
    animation: none;
  }
  /* The mark still has to be visible; it just does not fade. The log takes it
     off again itself once the row has stopped being the new one. */
  tr.row-new > td {
    animation: none;
    background: var(--accent-soft);
  }
  tr.row-new > td:first-child {
    box-shadow: inset 2px 0 0 var(--accent);
  }
}

/* --------------------------------------------------------------- modal */

.overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgb(10 10 12 / 45%);
  display: grid;
  place-items: center;
  padding: 20px;
  animation: fade 0.12s ease-out;
}
@keyframes fade {
  from {
    opacity: 0;
  }
}

.modal {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow);
  width: min(520px, 100%);
  max-height: calc(88vh / var(--ui-scale));
  display: flex;
  flex-direction: column;
}
.modal-wide {
  width: min(680px, 100%);
}
.modal-head {
  padding: 16px 18px 12px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: flex-start;
  gap: 10px;
}
.modal-head .spacer {
  flex: 1;
}
.modal-body {
  padding: 18px;
  overflow-y: auto;
}
/* Nothing carries its own margin into the footer's border. A dialog ending on
   a banner or a field otherwise has a band of empty space under it that the
   ones ending on a list do not. */
.modal-body > :last-child,
.modal-body > form > :last-child,
.modal-body > .stack > :last-child {
  margin-bottom: 0;
}
.modal-foot {
  padding: 13px 18px;
  border-top: 1px solid var(--border);
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  background: var(--surface-2);
  border-radius: 0 0 12px 12px;
}

/* -------------------------------------------------------------- toasts */

.toasts {
  position: fixed;
  bottom: 18px;
  right: 18px;
  z-index: 200;
  display: grid;
  gap: 8px;
  max-width: 380px;
}
.toast {
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  padding: 10px 13px;
  font-size: 13px;
  animation: rise 0.15s ease-out;
}
.toast.bad {
  border-left-color: var(--bad);
}
.toast.good {
  border-left-color: var(--good);
}
@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
}

/* ------------------------------------------------------------- sign in */

.signin {
  min-height: calc(100vh / var(--ui-scale));
  display: grid;
  place-items: center;
  padding: 24px;
}
.signin-card {
  width: min(400px, 100%);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: var(--shadow);
  padding: 30px;
}
.signin-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 22px;
  font-size: 17px;
  font-weight: 620;
  letter-spacing: -0.02em;
}
/* One button per identity provider, stacked. A deployment with one directory
   has a single button here and the gap never applies. */
.signin-providers {
  display: grid;
  gap: 10px;
}
.signin-note {
  font-size: 12.5px;
  color: var(--muted);
  margin-top: 18px;
  line-height: 1.55;
}
.divider {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--faint);
  font-size: 11.5px;
  margin: 20px 0;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.divider::before,
.divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

.banner {
  padding: 10px 13px;
  border-radius: var(--radius-sm);
  font-size: 12.5px;
  margin-bottom: 16px;
  line-height: 1.5;
}
.banner-bad {
  background: var(--bad-soft);
  color: var(--bad);
}
.banner-warn {
  background: var(--warn-soft);
  color: var(--warn);
}
.banner-info {
  background: var(--accent-soft);
  color: var(--accent);
}
.banner-good {
  background: var(--good-soft);
  color: var(--good);
}

/* --------------------------------------------------------------- connect */

/* Numbered steps: a developer works down this screen once, so the order is the
   structure and the numbers are the only chrome it needs. */
.steps {
  display: grid;
  gap: 14px;
}

.step {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
}

.step-n {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 12px;
  font-weight: 650;
  flex: none;
}

/* The first-run checklist reuses the step layout, with two states the connect
   screen has no use for: what is already done, and what to do next. */
.step-done {
  opacity: 0.62;
}
.step-done .step-n {
  background: var(--good-soft);
  color: var(--good);
}
.step-done .step-n svg {
  width: 13px;
  height: 13px;
}
.step-now {
  border-color: var(--accent);
}

.step-body {
  min-width: 0;
}
.step-body p {
  margin: 0 0 10px;
}
.step-body p:last-child {
  margin-bottom: 0;
}
.step-body code {
  background: var(--surface-3);
  border-radius: 4px;
  padding: 1px 5px;
}

/* A copyable block. It scrolls rather than wrapping: a config with a soft-
   wrapped URL in it reads as two lines that are not there. */
.code {
  position: relative;
}
.code pre {
  margin: 0;
  background: var(--surface-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 12px 44px 12px 12px;
  overflow-x: auto;
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.55;
  tab-size: 2;
}
.code pre code {
  background: none;
  padding: 0;
  font-size: inherit;
}
.code-copy {
  position: absolute;
  top: 7px;
  right: 7px;
  background: var(--surface);
  z-index: 1;
}

/* ------------------------------------------------- collapsed sidebar */

@media (min-width: 991px) {
  .shell.nav-collapsed {
    grid-template-columns: 60px 1fr;
  }
  .nav-collapsed .sidebar {
    padding: 18px 10px;
  }
  .nav-collapsed .nav-text {
    display: none;
  }
  /* Collapsed there is no room for the headings, so a rule carries the
     grouping instead - without it the icons run together as one list. */
  .nav-collapsed .nav-label {
    display: none;
  }
  .nav-collapsed .nav-group + .nav-group {
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px solid var(--border);
  }
  .nav-collapsed .brand {
    justify-content: center;
    padding: 5px 0;
    gap: 0;
  }
  .nav-collapsed .nav-item {
    justify-content: center;
    padding: 8px 0;
  }
  .nav-collapsed .nav-collapse svg {
    transform: rotate(180deg);
  }
  .nav-collapsed .account {
    flex-direction: column;
    background: none;
    border: 0;
    padding: 4px 0;
  }
  .nav-collapsed .account-id {
    display: none;
  }
}

/* ------------------------------------------------------------------- map */

/* The live map. The drawing is on a canvas - the panel's one piece of
   imperative rendering, and the one screen where hundreds of things move a
   minute - so what is styled here is the frame around it: the card it sits in,
   the tooltip that has to be DOM to be read at all, and the key to what the
   colours on it mean. */

.map-page {
  display: grid;
  gap: 16px;
}

.map-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  flex-wrap: wrap;
}
.map-head .spacer {
  flex: 1;
}
.map-title {
  display: grid;
  gap: 3px;
}

/* The canvas is drawn at the card's own width and scrolls sideways rather than
   being squeezed: a map narrower than this stops being readable long before it
   stops fitting. */
.map-holder {
  position: relative;
  padding: 0;
  overflow-x: auto;
}
.map-canvas {
  display: block;
}

.map-tip {
  position: absolute;
  z-index: 2;
  width: 236px;
  pointer-events: none;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  padding: 9px 11px;
  font-size: 12px;
}
.map-tip-title {
  font-weight: 600;
  margin-bottom: 5px;
}
.map-tip dl {
  display: grid;
  gap: 3px;
  margin: 0;
}
.map-tip dl > div {
  display: flex;
  align-items: baseline;
  gap: 8px;
}
.map-tip dt {
  flex: 1;
  min-width: 0;
  color: var(--muted);
}
.map-tip dd {
  margin: 0;
  text-align: right;
  white-space: nowrap;
}

.map-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  font-size: 12px;
  color: var(--muted);
}
.map-key {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.map-key i {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex: none;
}
.map-key-ok i {
  background: var(--accent);
}
.map-key-refused i {
  background: var(--warn);
}
.map-key-failed i {
  background: var(--bad);
}
/* The last two are the boundary rather than an outcome, so they are the lines
   that cross it and not two more dots. */
.map-key-inside i,
.map-key-outside i {
  width: 22px;
  height: 0;
  border-radius: 0;
  border-top: 2px solid var(--border-strong);
}
.map-key-outside i {
  border-top-style: dashed;
  border-top-color: var(--warn);
}

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

/* ------------------------------------------------------------ responsive */

@media (max-width: 990px) {
  /* The sidebar leaves the grid here, so the shell is the main pane alone and
     the page runs the full width of the screen. */
  .shell {
    grid-template-columns: 1fr;
    grid-template-rows: minmax(0, 1fr);
  }
  /* The drawer: the same sidebar, the same list in the same order, parked off
     the left edge until the hamburger brings it in. Its height is written the
     way the sticky sidebar's is, because the page is zoomed by --ui-scale and
     a viewport unit is not. */
  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 60;
    width: min(264px, 82vw);
    height: calc(100vh / var(--ui-scale));
    box-shadow: var(--shadow);
    transform: translateX(-101%);
    /* Hidden as well as moved away, so a closed drawer is not a dozen links
       the keyboard tabs into off the side of the screen. */
    visibility: hidden;
    transition:
      transform 0.18s ease,
      visibility 0.18s;
  }
  .nav-open .sidebar {
    transform: none;
    visibility: visible;
  }
  .nav-scrim {
    display: block;
    position: fixed;
    top: 0;
    left: 0;
    z-index: 50;
    width: calc(100vw / var(--ui-scale));
    height: calc(100vh / var(--ui-scale));
    background: rgb(23 23 26 / 42%);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease;
  }
  .nav-open .nav-scrim {
    opacity: 1;
    pointer-events: auto;
  }
  .btn.nav-toggle {
    display: inline-flex;
    flex: none;
  }
  /* Collapsing a drawer to a strip of icons would save nothing: it is over the
     page either way, and it is gone as soon as it has been used. */
  .nav-collapse {
    display: none;
  }
  .grid-4 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .grid-3,
  .grid-2 {
    grid-template-columns: 1fr;
  }
  .content,
  .topbar {
    padding-left: 16px;
    padding-right: 16px;
  }
}

@media (max-width: 616px) {
  /* The topbar now carries the hamburger as well, so it gives up what it can:
     tighter padding, and a page title that truncates rather than pushing the
     organisation and the theme off the end of the row. */
  .topbar {
    gap: 8px;
    padding: 12px 16px;
  }
  .topbar .stack {
    min-width: 0;
  }
  #page-title {
    font-size: 17px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .topbar .select {
    max-width: 36vw;
  }
  .grid-4 {
    grid-template-columns: 1fr;
  }
  .fact {
    grid-template-columns: 1fr;
    gap: 2px;
  }
  .field-row {
    grid-template-columns: 1fr;
  }
  .step {
    grid-template-columns: 1fr;
    gap: 10px;
  }
  .toolbar-search {
    flex: 1 1 100%;
  }
  /* The map's controls stop sharing a line with its heading; the canvas
     itself keeps its width and scrolls, which is what it is for. */
  .map-head {
    flex-direction: column;
    align-items: stretch;
  }
}
