/* golgift-sentry dashboard.

   Dark only, and deliberately so: this is an operations console that sits on a wall or a second
   monitor, and a light theme would be a second palette to keep honest for no one who asked.

   Colours are tokens, never literals, with one exception — the state colours below are also
   spelled as rgba() fills and borders, because a translucent wash cannot be derived from a hex
   custom property without color-mix() support we do not need to require. */

:root {
  /* surfaces, back to front */
  --chrome: #0c0e11;
  --bg: #0f1114;
  /* Between bg and surface. Its one job is to shade a continuation row in the overview's
     app/environment list, so the eye reads two environments as belonging to one app. */
  --bg-alt: #111317;
  --surface: #16181d;
  --raised: #1b1e24;

  --border: #262a32;
  --border-soft: #1f232a;
  --border-lit: #3b4a63;
  /* The border a secondary control wears. Lighter than --border, which draws the boxes: rank
     is what it marks, not availability, so a Cancel beside a primary reads as the other
     choice rather than as something switched off. */
  --border-control: rgba(152, 161, 176, 0.55);

  /* text, brightest to faintest */
  --text: #e9ebef;
  --muted: #98a1b0;
  --dim: #666e7c;
  --faint: #4d5460;
  /* A control or a dot that is present and doing nothing — a quiet environment's dot, a
     type-to-confirm primary before the name matches. Dimmer than --faint, because it must not
     read as "clickable but quiet". */
  --off: #3a4150;

  --accent: #6ea8fe;
  --accent-lit: #9dc3ff;
  --accent-deep: #2f6ddb;

  /* One per group state, so a badge and the rail beside it cannot disagree. `closed` is grey
     rather than a hue: it is the one state that is not asking for anything. */
  --state-new: #ffbf6b;
  --state-resurfaced: #ff7d8f;
  --state-resolved: #7ee2a8;
  --state-closed: #7d8798;

  --paused: #c99bff;
  --danger: #ff8b8b;
  --ok: #7ee2a8;

  --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
  --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', ui-sans-serif, system-ui, sans-serif;

  --rail-width: 232px;
  --topbar-height: 52px;
  /* The narrow artboards size every tap target at 44px. Named because three separate
     controls have to agree on it, not because it varies. */
  --touch: 44px;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 400 14px/1.45 var(--sans);
  -webkit-font-smoothing: antialiased;
}

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

code, .mono, .token { font-family: var(--mono); }

.eyebrow {
  font: 500 10px/1 var(--mono);
  letter-spacing: 0.14em;
  color: var(--dim);
  text-transform: uppercase;
}

.dim { color: var(--dim); }
.num { text-align: right; }

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

/* Topbar spans both columns; the rail and the main pane share the row below it. One grid rather
   than a fixed-position topbar, so the rail scrolls with its own overflow and nothing has to
   reserve space for a bar it cannot measure. */
.shell {
  display: grid;
  grid-template-columns: var(--rail-width) minmax(0, 1fr);
  grid-template-rows: var(--topbar-height) minmax(0, 1fr);
  min-height: 100vh;
}

/* Signed out there is no rail, so the grid must not reserve its column. */
.shell.is-bare { grid-template-columns: minmax(0, 1fr); }

/* The scope switcher and its checkbox exist for narrow viewports only — see the fold at the
   bottom of this file. Above it there is nothing to toggle, so both are gone and the rail is
   simply a grid item. Hidden with `display: none` rather than clipped, so the box is not a
   focusable control with no visible effect. */
.sheet-toggle,
.sheet-summary { display: none; }

.topbar {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  height: var(--topbar-height);
  padding: 0 18px 0 20px;
  background: var(--chrome);
  border-bottom: 1px solid var(--border-soft);
}

.brand {
  display: flex;
  align-items: center;
  gap: 9px;
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.brand:hover { color: var(--text); }

.brand::before {
  content: '';
  width: 9px;
  height: 9px;
  border-radius: 2px;
  background: var(--accent);
  flex: none;
}

.topbar-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
.topbar-right { display: flex; align-items: center; gap: 10px; }

.topbar-divider { width: 1px; height: 24px; background: var(--border-soft); }

/* Says which half of the dashboard you are standing in, next to the brand. Purple, like every
   other admin affordance. */
.area-tag {
  font: 500 10px/1 var(--mono);
  letter-spacing: 0.14em;
  color: var(--paused);
  border-left: 1px solid var(--border);
  padding-left: 12px;
  text-transform: uppercase;
  white-space: nowrap;
}

/* The signed-in user, and the panel behind them. The only thing anchoring the panel is this
   box, so it keeps its own positioning context. */
.account-menu { position: relative; display: flex; align-items: center; }

.account {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 5px 9px 5px 5px;
  border-radius: 6px;
  color: var(--text);
  cursor: pointer;
  list-style: none;
}

/* The default disclosure triangle, in both spellings. The caret is drawn in the markup so it
   can sit after the name rather than before the avatar. */
.account::-webkit-details-marker { display: none; }
.account::marker { content: ''; }

.account:hover { background: var(--surface); }
.account-menu[open] .account { background: var(--raised); }

.account-name { font-size: 13px; font-weight: 500; }
.account-caret { font: 400 11px/1 var(--mono); color: var(--dim); }

.avatar {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--raised);
  color: var(--muted);
  font: 600 11px/1 var(--mono);
  text-transform: uppercase;
  flex: none;
}

.avatar.is-large { width: 38px; height: 38px; border-radius: 7px; color: var(--text); font-size: 13px; }

/* Lit while the panel is open: the chip is the one control on the page that is currently
   holding something, and the avatar is the part of it the eye came in on. */
.account-menu[open] .account .avatar { background: var(--border); border-color: var(--border-lit); color: var(--text); }

.account-panel {
  position: absolute;
  top: 44px;
  right: 0;
  z-index: 20;
  width: 212px;
  padding: 5px;
  background: var(--surface);
  border: 1px solid var(--border-lit);
  border-radius: 9px;
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.6);
}

/* Says whose menu this is, which the chip cannot once the panel covers it on a narrow
   viewport — and it is the only place the role is spelled out. */
.account-who {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 9px 10px;
  margin-bottom: 5px;
  border-bottom: 1px solid var(--border-soft);
}

.account-who .avatar { width: 28px; height: 28px; border-radius: 6px; background: var(--border); border-color: var(--border-lit); color: var(--text); }

.account-who-name { display: block; font-size: 12px; font-weight: 600; }
.account-role { display: block; margin-top: 3px; font: 400 10px/1 var(--mono); letter-spacing: 0.08em; color: var(--dim); text-transform: uppercase; }

/* One row shape for all three, whether it is a link or the button in the sign-out form. */
.account-item {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  margin: 0;
  padding: 8px 9px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text);
  font: 400 13px/1.45 var(--sans);
  text-align: left;
  cursor: pointer;
}

.account-item:hover { background: var(--raised); color: var(--text); }

.account-item.is-admin { color: var(--paused); }
.account-item.is-admin:hover { color: var(--paused); }

.account-item.is-quiet { color: var(--muted); }
.account-item.is-quiet:hover { color: var(--text); }

.account-tag { margin-left: auto; font: 500 9px/1 var(--mono); letter-spacing: 0.1em; color: var(--dim); text-transform: uppercase; }

.account-rule { height: 1px; margin: 5px 0; background: var(--border-soft); }

/* --- rail --------------------------------------------------------------------------------- */

.rail {
  display: flex;
  flex-direction: column;
  gap: 22px;
  padding: 18px 14px;
  background: var(--chrome);
  border-right: 1px solid var(--border-soft);
  overflow-y: auto;
}

.rail-group { display: flex; flex-direction: column; gap: 2px; }
.rail-group > .eyebrow { padding: 0 6px 8px; }

.rail-heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 0 6px 8px;
}

/* No dot: the 5 px dot means environment state, and a navigation row has none. Where you are
   is an inset rule down the left edge instead — it cannot be mistaken for a status glyph, and
   the padding is written for it so the label does not shift when it appears. */
.rail-item {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 8px 7px 11px;
  border-radius: 6px;
  color: var(--muted);
  font-size: 13px;
  font-weight: 500;
}

.rail-item:hover { background: var(--surface); color: var(--text); }
.rail-item.is-current { background: var(--raised); color: var(--text); box-shadow: inset 2px 0 0 var(--accent); }

.rail-app {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 6px 8px;
  border-radius: 6px;
  color: var(--text);
  font-size: 13px;
}

.rail-app:hover { background: var(--surface); color: var(--text); }
.rail-app > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.rail-env {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 8px 5px 20px;
  border-radius: 6px;
  color: var(--muted);
  font: 400 12px/1 var(--mono);
}

.rail-env:hover { background: var(--surface); color: var(--text); }
.rail-env.is-current { background: var(--raised); color: var(--text); }

.rail-empty { padding: 0 6px; font: 400 11px/1.6 var(--mono); color: var(--faint); }

/* Pinned to the bottom of the rail: what the rail is, and is not, for. */
.rail-note {
  margin-top: auto;
  padding-top: 12px;
  border-top: 1px solid var(--border-soft);
  font: 400 10px/1.6 var(--mono);
  color: var(--faint);
}

/* --- main --------------------------------------------------------------------------------- */

.main {
  display: flex;
  flex-direction: column;
  gap: 24px;
  padding: 26px 30px 34px;
  min-width: 0;
}

.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
}

/* Where the subtitle is a paragraph rather than a line of counts. Bottom-aligning an action
   against wrapping prose would drift it down the page as the text reflowed. */
.page-head.is-top { align-items: flex-start; }

.page-title { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.page-title h1 { margin: 0; font-size: 22px; font-weight: 600; letter-spacing: -0.01em; }
.page-title h1.mono { font: 600 20px/1.3 var(--mono); }

.subtitle { font: 400 12px/1.5 var(--mono); color: var(--dim); }

.crumbs { display: flex; align-items: center; gap: 7px; font: 400 11px/1 var(--mono); color: var(--dim); flex-wrap: wrap; }
.crumbs a { color: var(--accent); }
.crumbs a:hover { color: var(--accent-lit); }

.section { display: flex; flex-direction: column; gap: 9px; min-width: 0; }
.section-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
/* The label names the section and never wraps; the note beside it is what gives up the room. */
.section-head > .eyebrow { white-space: nowrap; flex: none; }
.section-head.is-split { justify-content: space-between; }
.section-note { font: 400 10px/1.5 var(--mono); color: var(--faint); }


/* What a list ends with when the next thing to do is one button: the action, and the sentence
   that says who the action is for, on the same line. */
.section-foot { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding-top: 18px; border-top: 1px solid var(--border-soft); }
.section-foot .hint { margin: 0; }

/* A row of things that sit on one baseline — a name and the badge that qualifies it. */
.inline-row { display: flex; align-items: center; gap: 10px; min-width: 0; flex-wrap: wrap; }
.lead { margin: 0; font-size: 15px; line-height: 1.45; }

/* Underline tabs, for the two halves of the admin area. */
.tabs { display: flex; gap: 20px; border-bottom: 1px solid var(--border-soft); }
.tab { padding-bottom: 12px; margin-bottom: -1px; color: var(--muted); font-size: 13px; font-weight: 500; }
.tab:hover { color: var(--text); }
.tab.is-current { color: var(--text); font-weight: 600; border-bottom: 2px solid var(--accent); }

/* A pair of figures the page leads with — "6 new" beside "1 284 events / 60 min". */
.stats { display: flex; align-items: center; gap: 20px; flex: none; }
.stats .topbar-divider { height: 30px; background: var(--border); }
.stat { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; }
.stat strong { font: 600 15px/1 var(--mono); }
.stat span { font: 400 11px/1 var(--mono); color: var(--dim); }
.stat.state-new strong { color: var(--state-new); }

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

.controls { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* One look for every secondary control, whether it is a link, a button in a form, or a select.
   Buttons in this dashboard are mostly one-field POST forms, so they cannot share an element.

   Secondary and ghosted share one enabled grey, and the border is what tells them apart: a
   chip with a border is a standalone choice beside a primary — Cancel, Close, Exit admin — and
   a bare one is an action inside the row that owns it. Neither is dimmed, so a dim label in
   this dashboard always means unavailable. */
.chip,
button.chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin: 0;
  padding: 8px 13px;
  border: 1px solid var(--border-control);
  border-radius: 6px;
  background: none;
  color: var(--muted);
  font: 500 13px/1 var(--sans);
  cursor: pointer;
  white-space: nowrap;
}

.chip:hover,
button.chip:hover { color: var(--accent); border-color: var(--border-lit); }

.chip.is-current,
button.chip.is-current { color: var(--accent); border-color: var(--border-lit); background: rgba(110, 168, 254, 0.08); }

/* The row action: same shape, one step smaller, for the controls that live inside a list row. */
.chip.mono,
button.chip.mono { font: 500 11px/1 var(--mono); letter-spacing: 0.06em; padding: 6px 9px; border-radius: 5px; }

/* The second action on a row, when the first is the one you came for: present, and not
   competing with it. Rotating a token is rare and consequential — it should not read as the
   obvious click next to Copy. It keeps the enabled grey and gives up the border alone, so it
   reads as quieter rather than as unavailable. */
.chip.is-quiet,
button.chip.is-quiet { border-color: transparent; }
.chip.is-quiet:hover,
button.chip.is-quiet:hover { color: var(--text); border-color: var(--border-control); background: transparent; }

.chip.is-danger,
button.chip.is-danger { color: var(--danger); border-color: rgba(255, 139, 139, 0.3); }
.chip.is-danger:hover,
button.chip.is-danger:hover { color: var(--chrome); border-color: var(--danger); background: var(--danger); }

/* Filters and single-choice sets. A pill is a state you are in, a chip is an action you take —
   different shapes so a row of them is not read as a row of buttons. */
.pill,
button.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  padding: 7px 11px;
  border: 1px solid var(--border);
  border-radius: 20px;
  background: none;
  color: var(--muted);
  font: 500 12px/1 var(--mono);
  cursor: pointer;
  white-space: nowrap;
}

.pill:hover,
button.pill:hover { color: var(--accent); border-color: var(--border-lit); }

.pill.is-current,
button.pill.is-current { background: var(--accent); border-color: var(--accent); color: var(--chrome); }
.pill .pill-count { opacity: 0.65; }

/* Purple, like everything else that is about the admin half. */
.pill.is-privileged:hover { color: var(--paused); border-color: rgba(201, 155, 255, 0.5); }

/* Also a link: "New app" opens a dialog on a wide screen and leads to a page on a narrow one,
   and either way it is the same control in the same place as the submit it ends at. */
.primary,
button.primary {
  display: inline-flex;
  align-items: center;
  margin: 0;
  padding: 8px 14px;
  border: 0;
  border-radius: 6px;
  background: var(--accent);
  color: var(--chrome);
  font: 600 13px/1.2 var(--sans);
  white-space: nowrap;
  cursor: pointer;
}

.primary:hover,
button.primary:hover { background: var(--accent-lit); color: var(--chrome); }
.primary:active,
button.primary:active { background: var(--accent-deep); }
/* A primary that sits on a section's heading line rather than ending a form. */
.primary.is-small { padding: 6px 11px; font-size: 12px; font-weight: 500; }

/* Resolving is the one triage action the design gives its own colour: it is the outcome the
   page is trying to reach, and it reads as green everywhere else in the dashboard. */
button.resolve {
  margin: 0;
  padding: 8px 14px;
  border: 0;
  border-radius: 6px;
  background: var(--state-resolved);
  color: var(--chrome);
  font: 500 13px/1.2 var(--sans);
  cursor: pointer;
}

button.resolve:hover { background: #9dedbd; }

/* --- forms -------------------------------------------------------------------------------- */

.field { display: flex; flex-direction: column; gap: 7px; min-width: 0; }

label {
  font: 500 10px/1 var(--mono);
  letter-spacing: 0.14em;
  color: var(--dim);
  text-transform: uppercase;
}

input,
select {
  width: 100%;
  padding: 11px 12px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--surface);
  color: var(--text);
  font: 400 13px/1.2 var(--mono);
}

input:focus,
select:focus { outline: 0; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(110, 168, 254, 0.14); }

input::placeholder { color: var(--dim); }

.form-row { display: flex; align-items: flex-end; gap: 8px; flex-wrap: wrap; }
.form-row .field { flex: 1 1 160px; }
/* A select wants its own width, not an equal share of the row. */
.form-row .field.is-narrow { flex: 0 0 140px; }

.hint { margin: 0; font: 400 11px/1.6 var(--mono); color: var(--dim); }

/* --- confirmation dialogs ----------------------------------------------------------------- */

/* One shell, three tiers. Nothing mutates on first click: every rename, pause and delete opens
   this, and the tier decides the glyph, the border tint and the primary. Shape as well as
   colour — a round accent dot, a square violet one, a square danger one — so the three are
   still distinguishable in a screenshot in grey.

   A real <dialog>, which is most of the reason to use one: Esc, the focus trap and the scrim
   are the browser's. `dialog:target` below is the no-script fallback — see dialogs.js. */
.dialog {
  width: min(480px, calc(100vw - 32px));
  max-height: calc(100vh - 48px);
  padding: 0;
  border: 1px solid #333a46;
  border-radius: 12px;
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6);
  overflow: hidden;
}

.dialog::backdrop { background: rgba(6, 7, 9, 0.72); }
.dialog > form { display: flex; flex-direction: column; max-height: calc(100vh - 48px); }

/* The one dialog whose body is not a form — the channel roster, which holds a form per row —
   so the shell itself has to be the column its head, body and foot stack in. Only while it is
   open, or the display would defeat the browser's own way of keeping it shut. */
.dialog.is-pane[open],
.dialog.is-pane:target { display: flex; flex-direction: column; }

/* Tier one is an edit and wants no more room than its field. */
.dialog.tier-1 { width: min(420px, calc(100vw - 32px)); }
/* Except the ones carrying a list: the create-app form with its four fields and its grants,
   and the channel roster, whose rows hold an address that should not have to wrap. Wider, and
   the list scrolls inside the shell rather than growing the shell past the viewport. */
.dialog.is-wide { width: min(560px, calc(100vw - 32px)); }
.dialog.is-wide .grants { max-height: 30vh; overflow-y: auto; }
/* The dialog's own fill is `--surface`, so a panel inside it drops to the page colour to stay
   a panel rather than blending into its shell. */
.dialog .grant-group { background: var(--bg); }
/* Tier three is the only one that borrows the danger hue for its frame. */
.dialog.tier-3 { border-color: #3d2a2e; }

.dialog-head { display: flex; align-items: flex-start; gap: 12px; padding: 18px 20px 0; }

.dialog-glyph {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 6px;
  border: 1px solid;
  flex: none;
}

.dialog-glyph::before { content: ''; width: 8px; height: 8px; display: block; }

.dialog.tier-1 .dialog-glyph { background: rgba(110, 168, 254, 0.14); border-color: rgba(110, 168, 254, 0.5); }
.dialog.tier-1 .dialog-glyph::before { background: var(--accent); border-radius: 50%; }
.dialog.tier-2 .dialog-glyph { background: rgba(201, 155, 255, 0.14); border-color: rgba(201, 155, 255, 0.5); }
.dialog.tier-2 .dialog-glyph::before { background: var(--paused); border-radius: 1px; }
.dialog.tier-3 .dialog-glyph { background: rgba(255, 139, 139, 0.14); border-color: rgba(255, 139, 139, 0.5); }
.dialog.tier-3 .dialog-glyph::before { background: var(--danger); }

.dialog-said { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.dialog-said h2 { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
.dialog-said p { margin: 0; font-size: 13px; line-height: 1.5; color: var(--muted); }

.dialog-x { flex: none; font: 400 14px/1 var(--mono); color: var(--dim); }
.dialog-x:hover { color: var(--text); }

/* Indented to the title, so the body reads as the title's own rather than the glyph's. The
   inset is the glyph plus its gap. */
.dialog-body {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 14px 20px 0 58px;
  overflow-y: auto;
}

.dialog-body .field { gap: 6px; }
.dialog-body label { text-transform: none; letter-spacing: normal; font: 400 12px/1.4 var(--sans); color: var(--muted); }
.dialog-body label .mono { font: 600 12px/1 var(--mono); color: var(--text); }

/* One field asking for one of two things, and the chosen kind names it. Only the label and
   the hint swap, never the input, so what has been typed survives changing your mind.

   With `:has` unsupported both labels and both hints show — which still says what the field
   takes, so the dialog stays usable rather than becoming a mystery box. */
.dialog:has(.choices .choice:last-of-type:checked) .for-email { display: none; }
.dialog:has(.choices .choice:first-of-type:checked) .for-telegram { display: none; }

/* What an action spans, when it spans more than the thing you clicked. */
.dialog-scope {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 9px 11px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 7px;
  font: 400 12px/1.4 var(--mono);
  color: var(--text);
}

/* Tier three lists what goes, because "delete" is not a number and the number is the point. */
.dialog-blast { border: 1px solid rgba(255, 139, 139, 0.3); border-radius: 8px; background: rgba(255, 139, 139, 0.06); overflow: hidden; }
.dialog-blast > .eyebrow { display: block; padding: 7px 12px; border-bottom: 1px solid rgba(255, 139, 139, 0.2); color: var(--danger); }
.dialog-blast-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.dialog-blast-grid > div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 12px;
  border-top: 1px solid rgba(255, 139, 139, 0.12);
}

.dialog-blast-grid span { font: 400 11px/1.3 var(--mono); color: var(--muted); }
.dialog-blast-grid strong { font: 600 12px/1 var(--mono); }

/* One channel in the roster pane: kind, target, and what can be done to it. The row drops to
   the page colour inside the dialog's own fill, for the same reason a grant list does. */
.channel {
  display: grid;
  grid-template-columns: 58px minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  padding: 11px 13px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
}

.channel .badge { justify-content: center; }
.channel .list-name { font-size: 12px; }

/* Reported, not asked: the two states an alert fires on, and that resolve is not one. Badges
   rather than words, because they are the same two badges the lists carry. */
.fires-on {
  display: flex;
  align-items: center;
  gap: 9px;
  flex-wrap: wrap;
  padding: 11px 12px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 7px;
}

.dialog-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 18px 20px;
}

.dialog-hint { font: 400 10px/1 var(--mono); color: var(--faint); }
/* Cancel first, and the actions sit right however few of them there are. */
.dialog-actions { display: flex; gap: 8px; margin-left: auto; }
.dialog-actions .chip { padding: 9px 14px; border-radius: 7px; }

.dialog-primary {
  margin: 0;
  padding: 9px 14px;
  border: 0;
  border-radius: 7px;
  background: var(--accent);
  color: var(--chrome);
  font: 600 13px/1.2 var(--sans);
  cursor: pointer;
}

.dialog-primary:hover { background: var(--accent-lit); }
/* The roster's primary opens the other pane rather than submitting, so it is an anchor — and
   an anchor has to be told to fill the box a button fills by itself. */
a.dialog-primary { display: inline-flex; align-items: center; color: var(--chrome); }
a.dialog-primary:hover { color: var(--chrome); }
.dialog.tier-2 .dialog-primary { background: var(--paused); }
.dialog.tier-2 .dialog-primary:hover { background: #dab8ff; }
.dialog.tier-3 .dialog-primary { background: var(--danger); }
.dialog.tier-3 .dialog-primary:hover { background: #ffa6a6; }

/* Refusing, and looking it: tier three's primary stays out until the name matches. */
.dialog-primary:disabled,
.dialog.tier-2 .dialog-primary:disabled,
.dialog.tier-3 .dialog-primary:disabled {
  background: none;
  border: 1px solid var(--border-soft);
  color: var(--off);
  cursor: not-allowed;
}

/* No-script fallback. The opener is a link to the dialog's id, so the fragment alone can show
   it: fixed and centred over a scrim of its own, since ::backdrop only exists for a modal.
   Esc and a scrim click are what is lost; Cancel is a link back, and the server still checks
   the typed name. */
dialog.dialog:target { display: flex; position: fixed; inset: 0; margin: auto; z-index: 10; }

dialog.dialog:target::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  background: rgba(6, 7, 9, 0.72);
}

/* --- panels ------------------------------------------------------------------------------- */

.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
}

.panel-body { padding: 16px; display: flex; flex-direction: column; gap: 14px; }
.panel-body + .panel-body { border-top: 1px solid var(--border-soft); }

.columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; align-items: start; }

/* A bordered card that is a row in a list of its own — an environment in app settings. */
.card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 14px 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 9px;
}

.card-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }

/* A value the operator has to read off the screen: a token, an inline field's current value. */
.inline-value {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 11px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  font: 400 12px/1.4 var(--mono);
  color: var(--muted);
  min-width: 0;
}

/* Never clipped to an ellipsis: the value is masked to a fixed width already, and a credential
   cut off at the edge of its strip is one somebody copies half of by eye. */
.inline-value > .token-value { white-space: nowrap; line-height: 1.4; }
.inline-value > .is-trailing { margin-left: auto; flex: none; }
/* What can be done to the value, on the end of its row and as one thing. */
.inline-actions { display: flex; align-items: center; gap: 10px; }

/* The sign-in page and the password form: one panel, centred, nothing else on the page. */
.solo { width: 100%; max-width: 440px; margin: 8vh auto 0; }

.solo .panel { padding: 44px 36px; display: flex; flex-direction: column; gap: 26px; border-radius: 12px; background: var(--bg); }
.solo .panel-body { padding: 0; gap: 12px; }
.solo .solo-head { display: flex; flex-direction: column; gap: 10px; }
.solo .brand { font-size: 16px; }
.solo .brand::before { width: 11px; height: 11px; border-radius: 3px; }
/* The width of the form it ends, so its label sits in the middle rather than at the far left
   of a button that is most of the page. */
.solo button.primary { display: flex; justify-content: center; width: 100%; padding: 12px; border-radius: 7px; font-size: 14px; }
.solo-foot { font: 400 11px/1.6 var(--mono); color: var(--dim); }

/* --- state badges ------------------------------------------------------------------------- */

/* Colour, dot shape and rail all follow the state, so the three never contradict each other:
   a filled left rail means "open", and only new and re-surfaced get one. */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 7px;
  border: 1px solid var(--border);
  border-radius: 3px;
  font: 500 10px/1 var(--mono);
  letter-spacing: 0.1em;
  color: var(--muted);
  text-transform: uppercase;
  white-space: nowrap;
}

.badge::before { content: ''; width: 6px; height: 6px; background: currentColor; border-radius: 50%; flex: none; }
.badge.is-bare::before { display: none; }

.badge.state-new { color: var(--state-new); border-color: rgba(255, 191, 107, 0.5); background: rgba(255, 191, 107, 0.12); }
.badge.state-re_surfaced { color: var(--state-resurfaced); border-color: rgba(255, 125, 143, 0.5); background: rgba(255, 125, 143, 0.12); }
.badge.state-re_surfaced::before { border-radius: 0; }
.badge.state-resolved { color: var(--state-resolved); border-color: rgba(126, 226, 168, 0.4); background: rgba(126, 226, 168, 0.1); }
.badge.state-closed { color: var(--state-closed); border-color: var(--border); background: none; }
.badge.state-closed::before { border-radius: 1px; }

.badge.is-tiny { font: 600 9px/1 var(--mono); letter-spacing: 0.12em; padding: 3px 5px; }
.badge.is-privileged { color: var(--paused); border-color: rgba(201, 155, 255, 0.4); }
.badge.is-accent { color: var(--accent); border-color: rgba(110, 168, 254, 0.4); }

/* Hatched rather than solid: a paused environment is a deliberate hole in the data, and the
   stripes say "nothing is arriving here" more honestly than a flat chip. The same hatching
   marks an unsymbolicated sample, which is the same kind of fact — something is missing. */
.badge.is-paused {
  color: var(--paused);
  border-color: rgba(201, 155, 255, 0.45);
  background: repeating-linear-gradient(135deg, rgba(201, 155, 255, 0.16) 0 4px, transparent 4px 8px);
  letter-spacing: 0.08em;
}

.badge.is-paused::before { border-radius: 1px; }

.badge.is-unsymbolicated {
  color: var(--state-new);
  border-color: rgba(255, 191, 107, 0.5);
  background: repeating-linear-gradient(135deg, rgba(255, 191, 107, 0.16) 0 4px, transparent 4px 8px);
  letter-spacing: 0.08em;
  border-radius: 5px;
  padding: 5px 7px;
}

.badge.is-unsymbolicated::before { background: none; border: 2px solid currentColor; }

.count-badge {
  padding: 4px 7px;
  border-radius: 4px;
  background: var(--raised);
  border: 1px solid var(--border);
  color: var(--muted);
  font: 500 11px/1 var(--mono);
  white-space: nowrap;
}

.count-badge.is-hot { background: var(--state-new); border-color: var(--state-new); color: var(--chrome); }
.count-badge.is-warm { background: rgba(255, 191, 107, 0.14); border-color: rgba(255, 191, 107, 0.4); color: var(--state-new); }
/* Nothing to report, and saying "0" in a filled chip would claim otherwise. */
.count-badge.is-quiet { background: none; border-color: transparent; color: var(--dim); padding: 4px 0; }

/* --- sparklines --------------------------------------------------------------------------- */

/* Bars are absolute counts scaled to the tallest bucket in their own series, so a sparkline
   says "shape over this window", never "volume against another row". The height comes from the
   template because it is data; everything else is here. Bars flex rather than sit at a fixed
   width, so one series fills whatever column it is given — which is what lets a row fold to a
   full-width sparkline on a narrow screen without a second series resolution. */
.spark { display: flex; align-items: flex-end; gap: 2px; height: 30px; min-width: 0; }
.spark.is-small { height: 26px; }
.spark span { flex: 1; min-width: 1px; min-height: 1px; border-radius: 1px; background: var(--accent); display: block; }
.spark.is-quiet span { background: var(--off); }
/* Stands in for a series the aggregation returned nothing for. */
.spark-floor { width: 100%; height: 1px; }
.spark.state-new span { background: var(--state-new); }
.spark.state-re_surfaced span { background: var(--state-resurfaced); }
.spark.state-resolved span { background: var(--state-resolved); }
.spark.state-closed span { background: var(--state-closed); }

/* --- attention strip ---------------------------------------------------------------------- */

.attention {
  display: grid;
  grid-template-columns: 3px minmax(0, 1fr) 190px 120px 96px;
  align-items: center;
  gap: 18px;
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 9px;
  color: var(--text);
}

.attention:hover { border-color: var(--border-lit); color: var(--text); }

.attention-rail { align-self: stretch; background: var(--state-new); }
.attention.state-re_surfaced .attention-rail { background: var(--state-resurfaced); }

.attention-what { display: flex; flex-direction: column; gap: 5px; padding: 14px 0 14px 4px; min-width: 0; }
.attention-type { font: 600 13px/1 var(--mono); }
.attention-message { font-size: 13px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attention-where { font: 400 12px/1.5 var(--mono); color: var(--dim); }
.attention-count { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; padding-right: 16px; }
.attention-count strong { font: 600 15px/1 var(--mono); }
.attention-trend { font: 500 10px/1 var(--mono); color: var(--state-new); }
.attention.state-re_surfaced .attention-trend { color: var(--state-resurfaced); }

/* --- apps and environments ---------------------------------------------------------------- */

/* One aligned list rather than a card per app: the app name sits in a left gutter and every
   environment is a row on the same columns, so adding an app appends rows and nothing
   reflows. A grid of cards re-columned itself every time the roster changed. */
.env-rows { display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }

.env-head,
.env-row {
  display: grid;
  grid-template-columns: 200px minmax(0, 1fr) 168px 210px 132px 108px;
  align-items: center;
  gap: 18px;
  padding: 0 18px;
}

.env-head {
  padding: 9px 18px;
  background: var(--chrome);
  border-bottom: 1px solid var(--border-soft);
  font: 500 10px/1 var(--mono);
  letter-spacing: 0.14em;
  color: var(--dim);
  text-transform: uppercase;
}

.env-row { position: relative; border-top: 1px solid var(--border-soft); background: var(--bg-alt); color: var(--text); }
.env-row:hover { background: var(--surface); color: var(--text); }
/* The first environment of an app opens a group: a full-strength rule above it, and no shade. */
.env-row.is-first { border-top-color: var(--border); background: none; }
.env-rows > .env-head + .env-row { border-top: 0; }

.env-app { display: flex; align-items: center; gap: 9px; align-self: stretch; padding: 11px 0; min-width: 0; }
.env-app-name { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.env-app-name > span { font-size: 14px; font-weight: 600; letter-spacing: -0.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Says "same app as the row above" without repeating the name. */
.env-app-cont { font: 400 11px/1 var(--mono); color: var(--dim); padding-left: 2px; }

.env-name { display: flex; align-items: center; gap: 9px; padding: 11px 0; min-width: 0; }
.env-name .name { font: 500 12px/1 var(--mono); }

/* The whole row is the environment's link, and the app's settings link sits on top of it.
   Stretching one anchor over the row is what lets both be real links: nesting is not allowed,
   and two competing hit areas would make the row a lottery. */
.row-link { color: var(--text); }
.row-link::after { content: ''; position: absolute; inset: 0; }
.row-link:hover { color: var(--text); }
.is-above { position: relative; z-index: 1; }
.env-last { font: 400 11px/1.4 var(--mono); color: var(--muted); }
.env-last.is-quiet { color: var(--dim); }
.env-row .spark { height: 28px; }
.env-new { display: flex; align-items: center; justify-content: flex-end; }
.env-open { font: 600 15px/1 var(--mono); text-align: right; }
.env-open.is-quiet { color: var(--off); }

/* The one glyph that carries state without a label. A circle means ingest is live and the
   colour reports the loudest thing in the window; a square means it is paused — shape as well
   as colour, so it survives a screenshot in grey. */
.dot { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); flex: none; }
.dot.is-hot { background: var(--state-new); }
.dot.is-paused { background: var(--paused); border-radius: 1px; }
/* Live, and nothing open. */
.dot.is-off { background: var(--off); }

/* Wherever the dot appears in a list, the list says what its colours mean. Pushed to the far
   end of the heading it belongs to, and wrapping below the note when there is no room. It
   shrinks as well as wraps: on one column the four keys are wider than the viewport, and a
   legend that refused to give way took the whole page sideways with it. */
.dot-legend { margin-left: auto; display: flex; align-items: center; gap: 12px; flex: 0 1 auto; min-width: 0; flex-wrap: wrap; }
.dot-legend > span { display: inline-flex; align-items: center; gap: 6px; font: 400 10px/1 var(--mono); color: var(--dim); }

/* --- tables ------------------------------------------------------------------------------- */

/* Grid rather than <table>: every one of these lists has a coloured state rail as its first
   column, and a 3px <td> that stretches to the row height is a fight with the table layout. */
.rows { display: flex; flex-direction: column; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }

.rows-head,
.row {
  display: grid;
  grid-template-columns: 4px minmax(0, 1fr) 128px 92px 128px 132px;
  align-items: center;
  gap: 16px;
  padding: 0 30px 0 0;
}

.rows-head {
  padding: 9px 30px 9px 26px;
  background: var(--chrome);
  border-bottom: 1px solid var(--border-soft);
  font: 500 10px/1 var(--mono);
  letter-spacing: 0.14em;
  color: var(--dim);
  text-transform: uppercase;
}

.rows-head a { color: var(--dim); }
.rows-head a:hover { color: var(--accent); }
.rows-head a.is-current { color: var(--accent); }

.row { border-bottom: 1px solid var(--border-soft); color: var(--text); }
.row:hover { background: var(--raised); color: var(--text); }

.row-rail { align-self: stretch; }
.row.state-new .row-rail { background: var(--state-new); }
.row.state-re_surfaced .row-rail { background: var(--state-resurfaced); }

.row-what { display: flex; flex-direction: column; gap: 5px; padding: 15px 0 15px 22px; min-width: 0; }
/* Type and message share the first line: the type is the identity and never truncates, the
   message is the detail and gives up its tail first. */
.row-title { display: flex; align-items: center; gap: 10px; min-width: 0; }
.row-type { font: 600 14px/1.2 var(--mono); white-space: nowrap; }
.row-message { font-size: 13px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row-culprit { font: 400 11px/1 var(--mono); color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row-badge { justify-self: start; }
.row-count { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; }
.row-count strong { font: 600 14px/1 var(--mono); color: var(--muted); }
.row-count strong.is-hot { color: var(--text); }
.row-when { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; font: 400 12px/1 var(--mono); }
.row-when .then { font: 400 10px/1 var(--mono); color: var(--dim); }

.rows-foot { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 16px 30px; font: 400 11px/1 var(--mono); color: var(--dim); }

/* Simple two-column lists — channels, users, environments — with no state rail. */
.list { display: flex; flex-direction: column; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }

.list-name { font: 500 13px/1.3 var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list-meta { font: 400 11px/1.5 var(--mono); color: var(--dim); }
.list-actions { display: flex; align-items: center; gap: 6px; flex: none; flex-wrap: wrap; justify-content: flex-end; }

/* A row action: one square, one glyph, no word. Four labelled buttons on the end of a row read
   as a sentence and crowd out what the row is about, and the same four repeat on every row —
   so they are learned once and then take as little space as a state dot. Shape and colour do
   the sorting: ghost for the ones that change something back, danger fill for the ones that do
   not. See `icon_link` in _macros.html for the glyph set and the labels they carry. */
.icon-btn {
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: none;
  color: var(--muted);
  /* Well over the 13px the row's text runs at: these are drawings, not words, and a glyph set
     at body size reads as a smudge in a 30px square. The two bar glyphs are the exception —
     ❙❙ and ▶ are already solid at 12px and grow into a blob past it. */
  font: 400 17px/1 var(--mono);
  cursor: pointer;
  flex: none;
}

.icon-btn:hover { color: var(--accent); border-color: var(--border-lit); }

/* Pausing is the violet action wherever it appears. Resume is violet before it is hovered:
   the glyph is already reporting a paused target. */
.icon-btn.is-pause { font-size: 12px; }
.icon-btn.is-pause:hover { color: var(--paused); border-color: rgba(201, 155, 255, 0.5); }
.icon-btn.is-resume { font-size: 12px; color: var(--paused); border-color: rgba(201, 155, 255, 0.45); }
.icon-btn.is-resume:hover { color: var(--paused); border-color: var(--paused); }

.icon-btn.is-danger { color: var(--danger); border-color: rgba(255, 139, 139, 0.3); }
.icon-btn.is-danger:hover { color: var(--chrome); border-color: var(--danger); background: var(--danger); }

/* Optically matched rather than uniform: ⚙ is drawn small inside its own em box and ▷ is
   drawn large, so a single size would leave one of them looking wrong beside the other. */
.icon-btn.is-settings { font-size: 21px; }
.icon-btn.is-test { font-size: 16px; }

/* The one row action that carries a number: how many channels are listening is worth knowing
   without opening the dialog, so the glyph keeps its square and the count widens it. The
   count is a number to read, so it stays at the size of the row's other figures while the
   glyph beside it keeps the size of the other row actions. */
.icon-btn.is-counted { width: auto; padding: 0 9px; gap: 7px; display: flex; align-items: center; font: 400 12px/1 var(--mono); }
.icon-btn.is-counted::before { content: '\25c9'; font-size: 19px; }

.empty { padding: 22px 16px; font: 400 12px/1.6 var(--mono); color: var(--faint); text-align: center; }

/* A list whose rows share columns — the admin area's users and apps. */
.grid-head,
.grid-row {
  display: grid;
  align-items: center;
  gap: 14px;
  padding: 13px 26px;
  border-bottom: 1px solid var(--border-soft);
  color: var(--text);
}

.grid-head {
  padding: 9px 26px;
  background: var(--chrome);
  border-top: 1px solid var(--border-soft);
  font: 500 10px/1 var(--mono);
  letter-spacing: 0.14em;
  color: var(--dim);
  text-transform: uppercase;
}

.grid-row:hover { background: var(--surface); color: var(--text); }
.grid-row .num,
.grid-head .num { text-align: right; }

.users-grid { grid-template-columns: minmax(0, 1fr) 130px 150px 90px; }
/* The manage column is exactly its two icon buttons and the gap between them. */
.apps-grid { grid-template-columns: minmax(0, 1fr) 88px 110px 66px; }

.grid-who { display: flex; align-items: center; gap: 11px; min-width: 0; }
.grid-who > span:last-child { font-size: 13px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.grid-when { font: 400 12px/1 var(--mono); color: var(--muted); }
.grid-when.is-quiet { color: var(--dim); }
.grid-actions { display: flex; gap: 6px; justify-content: flex-end; flex-wrap: wrap; }
.grid-what { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.grid-title { display: flex; align-items: center; gap: 10px; min-width: 0; }
.grid-title > span:first-child { font-size: 14px; font-weight: 600; }
.grid-count { font: 600 14px/1 var(--mono); color: var(--muted); text-align: right; }

/* An environment as a chip in a list of them — the admin app list's second line. */
.env-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 7px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--surface);
  color: var(--muted);
  font: 400 11px/1 var(--mono);
  white-space: nowrap;
}

.env-chip:hover { color: var(--accent); border-color: var(--border-lit); }

.env-chip.is-paused {
  color: var(--paused);
  border-color: rgba(201, 155, 255, 0.45);
  background: repeating-linear-gradient(135deg, rgba(201, 155, 255, 0.16) 0 4px, transparent 4px 8px);
}

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

/* The page is its own frame: a chrome header carrying the identity and the triage actions,
   then the evidence beside a facts rail. The rail is chrome-coloured so it reads as part of
   the frame rather than as a second panel of content. */
.detail {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
}

.detail-head {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 22px 30px;
  background: var(--chrome);
  border-bottom: 1px solid var(--border-soft);
}

.detail-title { display: flex; align-items: flex-start; justify-content: space-between; gap: 28px; flex-wrap: wrap; }
.detail-what { display: flex; gap: 14px; min-width: 0; }
.detail-rail { width: 4px; align-self: stretch; border-radius: 2px; background: var(--border); flex: none; }
.detail.state-new .detail-rail { background: var(--state-new); }
.detail.state-re_surfaced .detail-rail { background: var(--state-resurfaced); }
.detail.state-resolved .detail-rail { background: var(--state-resolved); }
.detail-said { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.detail-said h1 { margin: 0; font: 600 22px/1.2 var(--mono); letter-spacing: -0.01em; }
.detail-since { font: 500 11px/1 var(--mono); color: var(--dim); }
.detail-culprit { font: 400 12px/1.4 var(--mono); color: var(--muted); overflow-wrap: anywhere; }
.detail-actions { display: flex; align-items: center; gap: 8px; flex: none; flex-wrap: wrap; }
.detail-actions > form { display: flex; align-items: center; gap: 8px; }
/* The triage actions the bar cannot hold on one column. Above the fold there is room for all
   of them, so the group is not a group at all — its buttons are the bar's own items. */
.actions-more { display: contents; }

.detail-body { display: flex; flex-direction: column; gap: 26px; padding: 24px 30px 34px; border-right: 1px solid var(--border-soft); min-width: 0; }
.detail-side { display: flex; flex-direction: column; gap: 22px; padding: 24px 22px; background: var(--chrome); min-width: 0; }

/* Key on the left, value right-aligned against it: a column of facts reads as a column, and
   a long value wraps under its own key rather than pushing the key off the row. */
.facts { display: flex; flex-direction: column; }
.facts > .eyebrow { padding-bottom: 8px; }
.fact { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--border-soft); }
.fact > dt { font: 400 11px/1.4 var(--mono); color: var(--dim); white-space: nowrap; }
.fact > dd { margin: 0; font: 400 12px/1.4 var(--mono); color: var(--muted); text-align: right; overflow-wrap: anywhere; }
.fact > dd.is-strong { color: var(--text); font-weight: 500; }
.fact > dd.state-new { color: var(--state-new); font-weight: 600; }
.fact > dd.state-re_surfaced { color: var(--state-resurfaced); font-weight: 600; }
.fact > dd.state-resolved { color: var(--state-resolved); font-weight: 600; }
.fact > dd.state-closed { color: var(--state-closed); font-weight: 600; }

.history { display: flex; flex-direction: column; gap: 9px; }
.history-row { display: grid; grid-template-columns: 9px minmax(0, 1fr); gap: 10px; align-items: start; }
.history-dot { width: 7px; height: 7px; border-radius: 50%; margin-top: 5px; background: var(--accent); }
.history-dot.state-new { background: var(--state-new); }
.history-dot.state-re_surfaced { background: var(--state-resurfaced); }
.history-dot.state-resolved { background: var(--state-resolved); }
.history-dot.state-closed { background: var(--state-closed); }
.history-what { display: flex; flex-direction: column; gap: 3px; padding-bottom: 12px; }
.history-what > span:first-child { font-size: 12px; color: var(--text); }
.history-when { font: 400 10px/1.3 var(--mono); color: var(--dim); }

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

/* The detail page's occurrence chart is an SVG that chart.js builds; these are its classes, so
   the names are its contract, not this file's. */
.chart-panel { display: flex; flex-direction: column; gap: 12px; }
.chart-wrap { padding: 16px 18px 12px; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; }
.chart { display: block; width: 100%; height: auto; }
.chart-grid { stroke: var(--border-soft); stroke-width: 1; }
.chart-label { fill: var(--dim); font: 400 10px/1 var(--mono); }
.chart-bar { fill: var(--accent); }
.chart-bar:hover { fill: var(--accent-lit); }
.chart-peak { font: 400 11px/1 var(--mono); color: var(--dim); }

/* --- samples, frames ---------------------------------------------------------------------- */

/* One sample at a time, switched by a radio group rather than by script: every stored sample
   is on the page, and the tabs decide which one is drawn. The pairing below is positional —
   the nth radio shows the nth article — which is why there is a rule per slot up to the ten
   samples `recent_events` returns. */
.samples { display: flex; flex-direction: column; gap: 12px; }
.sample-toggle { position: absolute; opacity: 0; pointer-events: none; }
.sample-tabs { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.sample-tabs .pills { display: flex; gap: 6px; flex-wrap: wrap; }
.sample-tabs label { text-transform: none; letter-spacing: normal; }

.sample { display: none; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
.sample-toggle:nth-of-type(1):checked ~ .sample:nth-of-type(1),
.sample-toggle:nth-of-type(2):checked ~ .sample:nth-of-type(2),
.sample-toggle:nth-of-type(3):checked ~ .sample:nth-of-type(3),
.sample-toggle:nth-of-type(4):checked ~ .sample:nth-of-type(4),
.sample-toggle:nth-of-type(5):checked ~ .sample:nth-of-type(5),
.sample-toggle:nth-of-type(6):checked ~ .sample:nth-of-type(6),
.sample-toggle:nth-of-type(7):checked ~ .sample:nth-of-type(7),
.sample-toggle:nth-of-type(8):checked ~ .sample:nth-of-type(8),
.sample-toggle:nth-of-type(9):checked ~ .sample:nth-of-type(9),
.sample-toggle:nth-of-type(10):checked ~ .sample:nth-of-type(10) { display: block; }

/* And the tab that goes with it, by the same positional pairing. */
.sample-toggle:nth-of-type(1):checked ~ .sample-tabs .pills label:nth-of-type(1),
.sample-toggle:nth-of-type(2):checked ~ .sample-tabs .pills label:nth-of-type(2),
.sample-toggle:nth-of-type(3):checked ~ .sample-tabs .pills label:nth-of-type(3),
.sample-toggle:nth-of-type(4):checked ~ .sample-tabs .pills label:nth-of-type(4),
.sample-toggle:nth-of-type(5):checked ~ .sample-tabs .pills label:nth-of-type(5),
.sample-toggle:nth-of-type(6):checked ~ .sample-tabs .pills label:nth-of-type(6),
.sample-toggle:nth-of-type(7):checked ~ .sample-tabs .pills label:nth-of-type(7),
.sample-toggle:nth-of-type(8):checked ~ .sample-tabs .pills label:nth-of-type(8),
.sample-toggle:nth-of-type(9):checked ~ .sample-tabs .pills label:nth-of-type(9),
.sample-toggle:nth-of-type(10):checked ~ .sample-tabs .pills label:nth-of-type(10) {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--chrome);
}

.sample-toggle:focus-visible ~ .sample-tabs .pills label { outline: 2px solid var(--accent); outline-offset: 2px; }

.sample-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--border-soft);
  flex-wrap: wrap;
}

.tags { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* Key and value in one chip, the key recessed: a row of these reads as a table without being
   one, and the SDK decides how many there are. */
.tag {
  display: inline-flex;
  align-items: center;
  background: var(--raised);
  border: 1px solid var(--border);
  border-radius: 5px;
  overflow: hidden;
  font: 400 11px/1 var(--mono);
  max-width: 100%;
}

.tag > span:first-child { color: var(--dim); padding: 5px 0 5px 7px; }
.tag > span:last-child { color: var(--text); padding: 5px 7px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sample-when { font: 400 11px/1 var(--mono); color: var(--dim); }

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

.frame {
  display: grid;
  grid-template-columns: 3px minmax(0, 1fr);
  border-bottom: 1px solid var(--border-soft);
}

.frame:last-child { border-bottom: 0; }
.frame-rail { align-self: stretch; }
.frame-what { display: flex; flex-direction: column; gap: 8px; padding: 11px 18px; min-width: 0; }
.frame-where { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.frame-path { font: 400 12px/1.4 var(--mono); color: var(--muted); overflow-wrap: anywhere; }
.frame-in { font: 400 12px/1.4 var(--mono); color: var(--dim); }
.frame-fn { font: 500 12px/1.4 var(--mono); color: var(--muted); overflow-wrap: anywhere; }

/* An in-app frame is the one worth reading: raised, with a rail, its path at full strength
   and its function in the accent. A vendor frame keeps the same row and gives all that up. */
.frame.is-app { background: var(--raised); }
.frame.is-app .frame-rail { background: var(--accent); }
.frame.is-app .frame-path { color: var(--text); font-weight: 600; }
.frame.is-app .frame-fn { color: var(--accent); }

/* A run of vendor frames, folded. Collapsed by default on every viewport: it is somebody
   else's stack, and it is only ever opened to prove that. */
.frames-fold { border-bottom: 1px solid var(--border-soft); }
.frames-fold:last-child { border-bottom: 0; }
.frames-fold > summary { display: flex; align-items: center; gap: 10px; padding: 11px 21px; font: 400 12px/1 var(--mono); color: var(--muted); cursor: pointer; list-style: none; }
.frames-fold > summary::-webkit-details-marker { display: none; }
.frames-fold > summary::before { content: '▸'; font-size: 11px; color: var(--dim); }
.frames-fold[open] > summary::before { content: '▾'; }
.frames-fold > summary:hover { background: var(--raised); }
.frames-fold .frame:last-child { border-bottom: 0; }

/* Source context for a symbolicated frame. Scrolls rather than wraps: a wrapped line of code
   loses the indentation that makes it readable, and the gutter would stop lining up. */
.context { display: flex; flex-direction: column; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
.context-line { display: grid; grid-template-columns: 52px minmax(0, 1fr); }
.context-line > span:first-child { font: 400 11px/1.8 var(--mono); color: var(--dim); text-align: right; padding-right: 12px; border-right: 1px solid var(--border); }
.context-line > code { font: 400 12px/1.65 var(--mono); color: var(--muted); padding-left: 12px; white-space: pre; overflow-x: auto; }
.context-line.is-culprit { background: rgba(255, 125, 143, 0.1); }
.context-line.is-culprit > code { color: #ffd0d7; font-weight: 500; }

/* A credential the operator has to copy. Wraps rather than scrolls: a token cut off at the
   panel edge is a token someone pastes half of. */
.token {
  display: block;
  padding: 11px 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
  font: 400 13px/1.4 var(--mono);
  overflow-wrap: anywhere;
  user-select: all;
}

/* Masked, and copied from the chip on the end. The summary is the strip itself, so the row
   reads the same whether or not it has been opened; the full value replaces nothing and
   appears underneath — which is where it goes when the clipboard is not available, since
   Copy has to leave the token reachable by hand. */
.copy { display: flex; flex-direction: column; gap: 10px; }
.copy > summary { cursor: pointer; list-style: none; }
.copy > summary::-webkit-details-marker { display: none; }
/* Hovering the strip offers Copy, which is what the row is for. Rotate stays quiet until it
   is hovered itself — see `.chip.is-quiet`. */
.copy > summary:hover .chip:not(.is-quiet) { color: var(--accent); border-color: var(--border-lit); }
.copy[open] .token-value { color: var(--dim); }

/* A remark riding along inside a value strip — what the credential is for, beside it. */
.inline-note { font: 400 11px/1.4 var(--mono); color: var(--faint); white-space: nowrap; }

/* --- create app --------------------------------------------------------------------------- */

/* A single-choice set of names. The radio itself is taken out of the flow rather than hidden,
   so it keeps its place in the tab order and its label stays a real control. */
.choices { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
/* These pills are labels, and a bare `label` carries the eyebrow's uppercase. An environment
   name is lowercase everywhere else in the dashboard. */
.choices .pill { text-transform: none; letter-spacing: normal; }
.choices .choice { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.choices .choice:checked + .pill { background: var(--accent); border-color: var(--accent); color: var(--chrome); }
.choices .choice:focus-visible + .pill { outline: 2px solid var(--accent-lit); outline-offset: 2px; }

/* Revealed by the last choice in that set. Shown by default so that a browser without :has
   support gets the field rather than a name it cannot enter — the server reads it only when
   "custom" is the choice either way. */
.choice-other { display: flex; }
/* Scoped to the field rather than the form: the same fields are rendered inside a dialog on
   the apps list, and the radio set and the text field it governs are siblings either way. */
.field:has(.choices .choice:not(:last-of-type):checked) .choice-other { display: none; }

.grant .badge { margin-left: auto; }

/* --- create user -------------------------------------------------------------------------- */

.user-fields { display: flex; flex-direction: column; gap: 16px; }

/* A single-choice set whose options need explaining. The role is the decision that matters
   when an account is created, and it is decided by what each one permits rather than by its
   name — so the names are stacked with what they mean rather than laid out as a pill row.
   The pill row stays on the user's own page, where the role is already known and being
   changed. The radio is taken out of the flow rather than hidden, so it keeps its place in
   the tab order and its label stays a real control. */
.choice-list { display: flex; flex-direction: column; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.choice-list .choice { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }

.choice-row { display: flex; align-items: flex-start; gap: 11px; padding: 11px 13px; border-bottom: 1px solid var(--border-soft); cursor: pointer; }
.choice-row:last-child { border-bottom: 0; }
.choice-row:hover { background: var(--surface); }

.choice-mark { width: 16px; height: 16px; flex: none; margin-top: 1px; display: grid; place-items: center; border: 1px solid var(--border); border-radius: 50%; }
.choice-mark::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: transparent; }

.choice:checked + .choice-row { background: rgba(110, 168, 254, 0.07); }
.choice:checked + .choice-row .choice-mark { border-color: var(--accent); }
.choice:checked + .choice-row .choice-mark::before { background: var(--accent); }
.choice:focus-visible + .choice-row { outline: 2px solid var(--accent-lit); outline-offset: -2px; }

.choice-said { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.choice-name { font: 500 12px/1 var(--mono); letter-spacing: 0.06em; color: var(--muted); }
.choice:checked + .choice-row .choice-name { color: var(--text); }
/* Purple, like everything else that is about the admin half — see `.pill.is-privileged`. */
.choice-row.is-privileged .choice-name { color: var(--paused); }
.choice-note { font-size: 12px; line-height: 1.5; color: var(--dim); }

/* An admin holds every environment by role, so the grid would be asking a question with no
   answer. Folded by the choice itself, like the custom-environment field on the app form: with
   `:has` unsupported the block stays, and `add_user` drops what it submits for an admin
   whether it was on screen or not. */
.user-fields:has(.choice[value='admin']:checked) .grant-block { display: none; }

/* What the pinned bar does not say for itself. Only above the fold: the narrow bar is two
   controls at tap size across the whole edge, and a third line of type under them would push
   the page's own last row further away for a remark it has already read. */
.bar-note { margin-left: auto; font: 400 10px/1 var(--mono); color: var(--faint); }

/* --- flashes ------------------------------------------------------------------------------ */

.flash {
  padding: 11px 14px;
  border: 1px solid var(--border);
  border-left-width: 3px;
  border-radius: 7px;
  background: var(--surface);
  font-size: 13px;
}

.flash-error { border-color: rgba(255, 139, 139, 0.35); background: rgba(255, 139, 139, 0.07); color: var(--danger); }
.flash-ok { border-color: rgba(126, 226, 168, 0.4); background: rgba(126, 226, 168, 0.07); color: var(--ok); }

/* --- grants grid -------------------------------------------------------------------------- */

/* A checkbox per environment, grouped by app, because a grant is only meaningful with the app
   name beside it — two apps with a `production` each are two different grants. */
.grants-form { display: flex; flex-direction: column; gap: 12px; align-items: stretch; }

/* Save, and the actions that are not it. Above the fold this is just the row under the grants
   and the ⋯ has nothing to offer, so it is not drawn; the narrow layout pins the bar to the
   bottom edge and the ⋯ becomes the way to the two cards it folds away. */
.sticky-bar { display: flex; align-items: center; gap: 8px; }
.overflow-toggle,
.overflow-summary { display: none; }
.grants { display: flex; flex-direction: column; gap: 10px; }
.grant-group { display: flex; flex-direction: column; background: var(--surface); border: 1px solid var(--border); border-radius: 9px; overflow: hidden; }
.grant-app { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--border-soft); font-size: 13px; font-weight: 600; }

.grant { display: flex; align-items: center; gap: 11px; padding: 9px 14px; border-bottom: 1px solid var(--border-soft); font: 500 12px/1 var(--mono); color: var(--muted); text-transform: none; letter-spacing: normal; cursor: pointer; }
.grant:last-child { border-bottom: 0; }
.grant:hover { background: var(--raised); }
.grant input { width: 15px; height: 15px; flex: none; accent-color: var(--accent); }
.grant:has(input:checked) { color: var(--text); }

/* --- narrow viewports --------------------------------------------------------------------- */

/* Below 1180 the widest column of each list goes — the one whose absence costs the least.
   Below 900 the desktop rows fold: nothing new is invented, and nothing that was a fact on a
   wide screen becomes unavailable on a narrow one. Sparklines in particular stay: they are
   the shape of the incident, and they flex to whatever width the fold gives them. */
@media (max-width: 1180px) {
  .rows-head,
  .row { grid-template-columns: 4px minmax(0, 1fr) 118px 92px 110px; }
  .rows-head > :nth-child(6),
  .row > .row-when { display: none; }
  .env-head,
  .env-row { grid-template-columns: 170px minmax(0, 1fr) 168px 132px 108px; }
  .env-head > :nth-child(4),
  .env-row > .spark { display: none; }
  .detail { grid-template-columns: minmax(0, 1fr) 280px; }
}

@media (max-width: 900px) {
  /* Topbar, then the scope switcher, then the page — one column, and every child takes the
     row it falls in. Implicit rows rather than a template, because the sheet is a row only
     while it is open and a fixed template would leave a gap where it was. */
  .shell,
  .shell.is-bare { grid-template-columns: minmax(0, 1fr); grid-template-rows: none; align-content: start; }

  .topbar { padding: 0 14px; position: sticky; top: 0; z-index: 3; }
  /* The name stays: a bar carrying one blue square and nothing else says nothing. In the
     admin area it gives way — the tag beside it is the more useful of the two, and with
     "Exit admin" on the same row there is not width for both. */
  .topbar .brand::before { width: 10px; height: 10px; }
  .topbar-left:has(.area-tag) .brand { font-size: 0; gap: 0; }
  .area-tag { border-left: 0; padding-left: 0; }
  /* Signed out the bar carries a lone square and the panel below it says the same name in
     full, so the page is the panel. */
  .shell.is-bare .topbar { display: none; }

  /* The chip folds to the avatar alone, at a tap size, and its panel comes up from the bottom
     edge instead of hanging off a corner that is now most of the screen's width. */
  .account-name,
  .account-caret { display: none; }
  .account { padding: 0; width: var(--touch); height: var(--touch); justify-content: center; }
  .account .avatar { width: 34px; height: 34px; border-radius: 7px; }

  .account-panel {
    position: fixed;
    top: auto;
    right: 0;
    bottom: 0;
    left: 0;
    width: auto;
    padding: 6px 10px 14px;
    border-width: 1px 0 0;
    border-radius: 12px 12px 0 0;
  }

  .account-item { min-height: var(--touch); }

  /* Visually hidden but still focusable and still a real checkbox; absolutely positioned so
     it claims no row of the grid. */
  .sheet-toggle {
    display: block;
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }

  .sheet-summary {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 10px 14px;
    background: var(--chrome);
    border-bottom: 1px solid var(--border-soft);
    cursor: pointer;
    text-transform: none;
    letter-spacing: normal;
  }

  .sheet-summary > span {
    display: flex;
    align-items: center;
    gap: 9px;
    flex: 1;
    min-width: 0;
    padding: 8px 10px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 7px;
    color: var(--text);
    font: 600 13px/1.2 var(--sans);
  }

  .sheet-summary > span::before { content: ''; width: 8px; height: 8px; border-radius: 2px; background: var(--accent); flex: none; }
  .sheet-summary > span::after { content: '▾'; margin-left: auto; font: 400 11px/1 var(--mono); color: var(--dim); }
  .sheet-toggle:checked ~ .sheet-summary > span::after { content: '▴'; }
  .sheet-toggle:focus-visible ~ .sheet-summary > span { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(110, 168, 254, 0.14); }

  /* The sheet itself: closed by default, because the page is what somebody came for. */
  .rail { display: none; border-right: 0; border-bottom: 1px solid var(--border-soft); max-height: 60vh; }
  .sheet-toggle:checked ~ .rail { display: flex; }
  .rail-note { display: none; }
  /* Same reason as the rail's note: it explains how the wide layout is put together, and on
     one column it is four lines of prose between a heading and the list it describes. */
  .section-note { display: none; }

  .main { padding: 16px 14px 24px; gap: 20px; }

  /* The page's heading becomes the screen's header: a strip of chrome under the switcher,
     edge to edge, rather than a 22px display line floating on the page. A phone has one
     column and no rail, so the title is what says where you are — and at that job it is a
     14px label like every other header here, not the biggest thing on the screen.

     Not `.is-top`: that class marks the heads whose subtitle is a paragraph, and six lines
     of prose is a page opening, not a bar. Those keep the smaller title and stay on the page. */
  .page-head { flex-direction: column; align-items: flex-start; }

  .page-head:not(.is-top) {
    gap: 11px;
    margin-left: -14px;
    margin-right: -14px;
    padding: 11px 14px;
    background: var(--chrome);
    border-bottom: 1px solid var(--border-soft);
  }

  /* Only when it opens the page. A flash got there first, and pulling the header over it
     would hide what the last click did. */
  .main > .page-head:not(.is-top):first-child { margin-top: -16px; }

  .page-title { gap: 4px; }
  .page-title h1,
  .page-title h1.mono { font-size: 14px; }
  .subtitle { font: 400 10px/1.5 var(--mono); }
  .page-head .primary { min-height: var(--touch); }
  .columns { grid-template-columns: minmax(0, 1fr); }
  .choices .pill { min-height: var(--touch); }

  /* A credential is never clipped, so the strip wraps instead: the masked value keeps its
     line and Copy and Rotate take the one below it. The remark beside the value is the one
     thing that goes — it says what the token is for, which the label already does. */
  .inline-value { flex-wrap: wrap; }
  .inline-note { display: none; }

  /* Attention: two lines of identity, then the sparkline at full width with the count beside
     it, then where it happened. Nothing dropped. */
  .attention { grid-template-columns: 3px minmax(0, 1fr) auto; gap: 0 10px; }
  .attention-rail { grid-area: 1 / 1 / -1 / 2; }
  .attention-what { grid-area: 1 / 2 / 2 / -1; padding: 12px 13px 0; gap: 8px; }
  .attention-message { white-space: normal; overflow: visible; line-height: 1.45; }
  .attention > .spark { grid-area: 2 / 2 / 3 / 3; padding: 8px 0 8px 13px; height: 26px; }
  .attention-count { grid-area: 2 / 3 / 3 / -1; flex-direction: row; align-items: baseline; gap: 7px; padding: 0 13px; }
  .attention-where { grid-area: 3 / 2 / 4 / -1; padding: 0 13px 12px; font-size: 10px; }

  /* Apps and environments: the app becomes a group header row of its own, and each
     environment folds to two lines with the sparkline keeping the full width. */
  /* On its own line the legend is no longer the far end of a heading, so it starts at the
     left edge like everything else in the column. */
  .dot-legend { margin-left: 0; }
  .env-head { display: none; }
  .env-row { grid-template-columns: minmax(0, 1fr) auto auto; gap: 9px 10px; padding: 11px 13px; }
  .env-row.is-first { background: none; }
  /* A continuation row's gutter says nothing on one column, so it does not take a line. */
  .env-app:has(.env-app-cont) { display: none; }
  .env-app { grid-area: 1 / 1 / 2 / -1; padding: 0; }
  .env-app-cont { display: none; }
  .env-name { grid-area: 2 / 1 / 3 / 2; padding: 0; }
  .env-open { grid-area: 2 / 2 / 3 / -1; }
  .env-row > .spark { grid-area: 3 / 1 / 4 / 2; display: flex; height: 22px; align-self: center; }
  .env-new { grid-area: 3 / 2 / 4 / 3; justify-content: flex-end; }
  .env-last { grid-area: 3 / 3 / 4 / -1; }

  /* Error list: title, message, then badge · sparkline · count on one line. */
  .rows-head { display: none; }
  .row { grid-template-columns: 3px auto minmax(0, 1fr) auto; gap: 0 10px; padding: 0; }
  .row-rail { grid-area: 1 / 1 / -1 / 2; }
  .row-what { grid-area: 1 / 2 / 2 / -1; padding: 12px 14px; gap: 8px; }
  .row-title { flex-direction: column; align-items: flex-start; gap: 6px; }
  .row-type { font-size: 13px; }
  .row-message { white-space: normal; overflow: visible; line-height: 1.4; }
  /* The only thing the fold gives up. The detail page carries it in full. */
  .row-culprit { display: none; }
  .row-badge { grid-area: 2 / 2 / 3 / 3; padding: 0 0 12px 14px; }
  .row > .spark { grid-area: 2 / 3 / 3 / 4; padding: 0 0 12px; height: 20px; align-self: end; }
  .row-count { grid-area: 2 / 4 / 3 / -1; flex-direction: row; align-items: baseline; gap: 6px; padding: 0 14px 12px 0; }
  .row-when { display: none; }
  .rows-foot { padding: 14px; }

  /* Filters scroll sideways rather than wrapping into four rows above the list. No bar under
     them: a chip half off the edge is what says there is more, and a scrollbar drawn across
     the page would be a second rule between the filters and the list. */
  .controls.is-scroller { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 2px; scrollbar-width: none; }
  .controls.is-scroller::-webkit-scrollbar { display: none; }
  .controls.is-scroller > * { flex: none; }

  /* Error detail: one column, facts in a two-up grid, and the triage actions pinned to the
     bottom of the viewport at 44px so they are reachable with a thumb. */
  .detail { grid-template-columns: minmax(0, 1fr); border: 0; border-radius: 0; }
  .detail-head { padding: 12px 14px; }
  .detail-title { gap: 12px; }
  .detail-said h1 { font-size: 16px; }
  .detail-since { display: none; }
  .detail-body { padding: 14px 14px 18px; border-right: 0; gap: 18px; }
  .detail-side { padding: 0 14px 18px; background: none; }
  .detail-actions {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 4;
    display: flex;
    align-items: stretch;
    gap: 8px;
    padding: 10px 14px 16px;
    background: var(--chrome);
    border-top: 1px solid var(--border);
  }

  /* The two triage actions somebody came to press share the bar, and the ⋯ beside them holds
     the rest — four buttons on one column are four buttons nobody can hit. */
  .detail-actions > form { display: flex; gap: 8px; flex: 1; }
  .detail-actions > form > button { flex: 1; min-width: 0; }
  .detail-actions button { height: var(--touch); padding: 0 12px; border-radius: 7px; justify-content: center; }

  /* Folded away until the ⋯ asks for them, and then a sheet off the bottom edge like every
     other thing that comes up from there. */
  .actions-more { display: none; }
  .overflow-toggle:checked ~ .actions-more {
    display: flex;
    flex-direction: column;
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(var(--touch) + 26px);
    z-index: 5;
    gap: 8px;
    padding: 14px;
    background: var(--chrome);
    border-top: 1px solid var(--border);
  }

  .actions-more > * { height: var(--touch); justify-content: center; text-align: center; }
  .actions-more > .chip { display: inline-flex; align-items: center; }
  .overflow-toggle:focus-visible ~ .overflow-summary { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(110, 168, 254, 0.14); }
  /* A dialog is fixed to the viewport, so it is not one of the bar's items and needs no
     room made for it here. */
  /* Room for the fixed bar, so the last fact is not sitting under it. */
  .main:has(.detail) { padding-bottom: calc(var(--touch) + 34px); }

  .facts { display: grid; grid-template-columns: 1fr 1fr; gap: 0 14px; }
  .facts > .eyebrow { grid-column: 1 / -1; }
  .fact { flex-direction: column; align-items: flex-start; gap: 4px; padding: 8px 0; }
  .fact > dd { text-align: left; }

  .context-line { grid-template-columns: 38px minmax(0, 1fr); }
  .context-line > span:first-child { padding-right: 8px; font-size: 10px; }
  .context-line > code { padding-left: 9px; font-size: 11px; }
  .frame-what { padding: 10px 12px; gap: 7px; }
  .frames-fold > summary { padding: 11px 15px; }

  /* Admin lists: stacked cards, actions on their own row. */
  .grid-head { display: none; }
  .grid-row { grid-template-columns: minmax(0, 1fr); gap: 10px; padding: 13px 16px; }
  .grid-row .num { text-align: left; }
  .grid-count { text-align: left; }
  .grid-actions { justify-content: flex-start; }

  /* Every row action goes to a tap size. Same square, same glyph — only bigger. */
  .icon-btn { width: var(--touch); height: var(--touch); border-radius: 7px; }
  /* Except the one carrying a count, which is as tall as the rest and as wide as it needs. */
  .icon-btn.is-counted { width: auto; padding: 0 12px; }

  /* Users: the avatar and name take the first line, the role and how much this account can
     see ride under it, and the row is a tap target end to end. */
  .grid-row.users-grid {
    grid-template-columns: minmax(0, 1fr) auto auto;
    gap: 7px 10px;
    align-items: center;
    min-height: var(--touch);
  }

  .grid-row.users-grid > .grid-who { grid-area: 1 / 1 / 2 / 3; }
  /* The row is a link and on one column there is room to say so. */
  .grid-row.users-grid::after {
    content: '›';
    grid-area: 1 / 3 / 2 / -1;
    justify-self: end;
    font: 400 13px/1 var(--mono);
    color: var(--off);
  }

  .grid-row.users-grid > .badge { grid-area: 2 / 1 / 3 / 2; justify-self: start; }
  .grid-row.users-grid > .grid-when.num { grid-area: 2 / 2 / 3 / 3; }
  .grid-row.users-grid > .grid-when:not(.num) { grid-area: 2 / 3 / 3 / -1; justify-self: end; }

  /* User detail: Save is the reason the page was opened, so it stops scrolling away, and the
     ⋯ beside it holds the password reset and the delete. */
  .sticky-bar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 4;
    padding: 10px 14px 16px;
    background: var(--chrome);
    border-top: 1px solid var(--border);
  }

  .sticky-bar > .primary { flex: 1; min-height: var(--touch); justify-content: center; }
  .sticky-bar > .chip { min-height: var(--touch); justify-content: center; }
  .bar-note { display: none; }

  .overflow-summary {
    display: grid;
    place-items: center;
    width: var(--touch);
    height: var(--touch);
    flex: none;
    border: 1px solid var(--border);
    border-radius: 7px;
    color: var(--muted);
    font-size: 22px;
    text-transform: none;
    letter-spacing: normal;
    cursor: pointer;
  }

  .overflow-toggle:focus-visible ~ .sticky-bar .overflow-summary { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(110, 168, 254, 0.14); }

  /* Folded away until the ⋯ asks for them, and then a sheet off the bottom edge like every
     other thing that comes up from there. */
  .columns.is-overflow { display: none; }
  .overflow-toggle:checked ~ .columns.is-overflow {
    display: grid;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 5;
    max-height: 84vh;
    overflow-y: auto;
    gap: 14px;
    padding: 16px 14px 22px;
    background: var(--chrome);
    border-top: 1px solid var(--border);
    border-radius: 12px 12px 0 0;
  }

  /* Room for the bar, so the last grant is not sitting under it. */
  .main:has(.sticky-bar) { padding-bottom: calc(var(--touch) + 34px); }

  /* Sign in: the card gives up its frame and becomes the page, and the two fields it has are
     worth the height. The panel carries the page's own padding rather than adding to it —
     stacked, the two insets pushed a 48px field into the middle third of the screen. */
  /* The rows above are packed to the top now, so the one page that wants the whole viewport
     asks for it rather than being handed the slack. */
  .main:has(.solo) { min-height: 100vh; padding: 0; justify-content: center; }
  .solo { margin: 0; max-width: none; }
  .solo .panel { padding: 34px 20px 26px; border: 0; border-radius: 0; }
  .solo input { height: 48px; padding: 0 13px; font-size: 14px; }
  .solo button.primary { height: 48px; padding: 0; }

  /* A dialog becomes a bottom sheet: full width, off the bottom edge, no side inset, and its
     actions stack at 44px so either one is reachable with a thumb. */
  .dialog,
  .dialog.tier-1,
  .dialog.is-wide {
    width: 100%;
    max-width: none;
    max-height: 88vh;
    margin: auto 0 0;
    border-radius: 22px 22px 0 0;
    border-bottom: 0;
  }

  /* The grab handle. Decorative here — there is no swipe to dismiss, which would be a gesture
     handler rather than a stylesheet — but it is what says "sheet" rather than "the page has
     been replaced", and the scrim and Esc both still cancel. */
  .dialog > form::before {
    content: '';
    width: 38px;
    height: 4px;
    margin: 10px auto 0;
    border-radius: 2px;
    background: var(--border-lit);
    flex: none;
  }

  .dialog > form { max-height: 88vh; }
  .dialog-head { padding: 12px 16px 0; }
  .dialog-body { padding: 14px 16px 0 16px; }
  /* A field at a tap size, and the value inside it big enough to read back. */
  .dialog-body input { height: 48px; padding: 0 12px; font-size: 14px; }
  .dialog-foot { flex-direction: column; align-items: stretch; gap: 8px; padding: 16px 16px 22px; }
  .dialog-hint { order: 1; text-align: center; }
  .dialog-actions { flex-direction: column; margin-left: 0; }
  /* The primary on top, where a thumb lands first: on a sheet this tall the actions are the
     end of a form somebody has just filled in, and reaching past Cancel to submit it is the
     wrong way round. Tier three still asks for the name to be typed before it will fire, so
     the destructive one is not made easier to hit by accident. */
  .dialog-actions .dialog-primary { order: -1; }
  .dialog-actions .chip,
  .dialog-primary { height: var(--touch); justify-content: center; text-align: center; }
  .dialog-actions .chip { display: inline-flex; align-items: center; }
  .dialog-blast-grid { grid-template-columns: minmax(0, 1fr); }
}
