/* SIPSorcery web front end.
 *
 * One stylesheet, no framework, no build step. The legacy site pulled in Bootstrap 2, a jQuery UI
 * theme, the jqGrid theme and three syntax highlighter themes, which between them defined the same
 * things several times and left the page's appearance depending on load order. There is not enough
 * here to justify any of that.
 *
 * Colours are defined once as tokens on :root and overridden only inside the dark block, so no colour
 * has its sole definition inside a media query. */

:root {
  --bg: #ffffff;
  --panel: #f8fafc;
  --panel-2: #f1f5f9;
  --fg: #0f172a;
  --muted: #64748b;
  --line: #e2e8f0;
  --line-strong: #cbd5e1;
  --accent: #1d4ed8;
  --accent-fg: #ffffff;
  --good: #15803d;
  --good-bg: #f0fdf4;
  --warn: #b45309;
  --warn-bg: #fffbeb;
  --bad: #b91c1c;
  --bad-bg: #fef2f2;
  --shadow: 0 1px 2px rgba(15, 23, 42, .06), 0 8px 24px rgba(15, 23, 42, .06);
  --radius: 8px;
  --mono: ui-monospace, "SF Mono", "Cascadia Mono", Consolas, monospace;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0b1120;
    --panel: #111827;
    --panel-2: #172033;
    --fg: #e2e8f0;
    --muted: #94a3b8;
    --line: #1e293b;
    --line-strong: #334155;
    --accent: #60a5fa;
    --accent-fg: #0b1120;
    --good: #4ade80;
    --good-bg: #0d2018;
    --warn: #fbbf24;
    --warn-bg: #241c07;
    --bad: #f87171;
    --bad-bg: #2a1111;
    --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 8px 24px rgba(0, 0, 0, .3);
  }
}

* { box-sizing: border-box; }

html { color-scheme: light dark; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

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

h1, h2, h3 { line-height: 1.25; margin: 0 0 .5em; font-weight: 600; letter-spacing: -.01em; }
h1 { font-size: 1.85rem; }
h2 { font-size: 1.3rem; }
h3 { font-size: 1.05rem; }

p { margin: 0 0 1em; }
code, pre, .mono { font-family: var(--mono); }

/* The browser's own `[hidden] { display: none }` is a user-agent rule, so any class here that sets
   display beats it - .btn does, and a hidden button carrying it stays on screen. Nothing that says
   it is hidden should be visible, which makes this one of the few places !important is the honest
   answer rather than a way around specificity. */
[hidden] { display: none !important; }

/* Only ever announced to assistive technology. Used for form labels whose meaning is carried
   visually by a column header. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---------------------------------------------------------------- chrome */

.site-header {
  border-bottom: 1px solid var(--line);
  background: var(--panel);
  position: sticky; top: 0; z-index: 20;
}

.site-header .bar {
  max-width: 1280px; margin: 0 auto; padding: 0 20px;
  display: flex; align-items: center; gap: 24px; min-height: 56px; flex-wrap: wrap;
}

.brand {
  font-weight: 650; font-size: 1.05rem; color: var(--fg);
  display: flex; align-items: center; gap: 9px; white-space: nowrap;
}
.brand:hover { text-decoration: none; }
/* The favicon itself, so the header and the browser tab carry the same mark. */
.brand .mark { width: 22px; height: 22px; border-radius: 5px; display: block; }

.site-nav { display: flex; gap: 18px; flex-wrap: wrap; }
.site-nav a { color: var(--muted); font-size: .92rem; }
.site-nav a:hover, .site-nav a[aria-current="page"] { color: var(--fg); text-decoration: none; }
.site-nav a[aria-current="page"] { font-weight: 600; }

.session { margin-left: auto; display: flex; align-items: center; gap: 12px; font-size: .9rem; }
.session .who { color: var(--muted); }
.session form { margin: 0; }

/* The admin area. The same bar with a stripe of the warning colour along its top, so the site that
   changes other people's accounts cannot be mistaken for the one that changes your own. */
.admin-header { border-top: 3px solid var(--warn); }
.admin-header .brand .badge { margin-left: 2px; }

.site-footer {
  margin-top: auto; border-top: 1px solid var(--line);
  background: var(--panel); color: var(--muted); font-size: .85rem;
}
.site-footer .bar {
  max-width: 1280px; margin: 0 auto; padding: 18px 20px;
  display: flex; gap: 18px; flex-wrap: wrap; align-items: center;
}
.site-footer a { color: var(--muted); }
.site-footer .copy { margin-left: auto; }
.site-footer .version { font-variant-numeric: tabular-nums; opacity: .8; }

main { width: 100%; }
.page { max-width: 1280px; margin: 0 auto; padding: 28px 20px 48px; width: 100%; }
.prose { max-width: 70ch; }
.prose h2 { margin-top: 1.6em; }
.prose ul { padding-left: 1.2em; }
.prose li { margin-bottom: .4em; }

/* Help pages. Code samples are dial plans people copy into their own, so they keep their line breaks
   and scroll sideways rather than wrap, which would change what a copied line says. */
.help-content { max-width: 80ch; min-width: 0; }
.help-content h2 { scroll-margin-top: 72px; }
.help-content h3 { margin-top: 1.6em; scroll-margin-top: 72px; }
.help-content pre {
  background: var(--panel); border: 1px solid var(--line); border-radius: 6px;
  padding: 10px 12px; overflow-x: auto; font-size: 13px; line-height: 1.5; margin: 0 0 1em;
}
.help-content :not(pre) > code {
  font-size: .9em; background: var(--panel-2); border-radius: 4px; padding: 1px 4px; overflow-wrap: anywhere;
}
.help-content blockquote {
  margin: 0 0 1em; padding: 8px 14px; border-left: 3px solid var(--line-strong); color: var(--muted);
  overflow-x: auto;
}
.help-content .table-wrap { margin-bottom: 1em; }
.portal-nav li.sub a { padding-left: 24px; font-size: .88rem; }

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

.portal { display: grid; grid-template-columns: 216px 1fr; gap: 32px; align-items: start; }

.portal-nav { position: sticky; top: 80px; }
.portal-nav ul { list-style: none; margin: 0; padding: 0; }
.portal-nav li { margin-bottom: 1px; }
.portal-nav a {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 7px 11px; border-radius: 6px; color: var(--muted); font-size: .92rem;
}
.portal-nav a:hover { background: var(--panel); color: var(--fg); text-decoration: none; }
.portal-nav a[aria-current="page"] {
  background: var(--panel-2); color: var(--fg); font-weight: 600;
}

@media (max-width: 860px) {
  .portal { grid-template-columns: 1fr; gap: 16px; }
  .portal-nav { position: static; }
  .portal-nav ul { display: flex; gap: 4px; flex-wrap: wrap; }
}

/* ------------------------------------------------------------ components */

.card {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--bg); margin-bottom: 24px; overflow: hidden;
}
.card > header {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 13px 16px; border-bottom: 1px solid var(--line); background: var(--panel);
}
.card > header h2 { margin: 0; font-size: 1rem; }
.card > header .count {
  font-size: .75rem; color: var(--muted); border: 1px solid var(--line-strong);
  border-radius: 10px; padding: 0 7px;
}
.card > header .spacer { margin-left: auto; }
.card > header .updated { color: var(--muted); font-size: .8rem; white-space: nowrap; }

/* A row of links that narrow a list, the current one marked. */
.filter-links { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.filter-links a[aria-current="page"] { border-color: var(--line-strong); background: var(--panel); font-weight: 600; }
.filter-links .count { color: var(--muted); font-weight: normal; margin-left: 4px; }
ul.plain { margin: 4px 0 0; padding-left: 18px; }
.card > .body { padding: 16px; }

.hint { color: var(--muted); font-size: .85rem; }
.card > header .hint { flex-basis: 100%; margin: 0; }

/* Buttons ---------------------------------------------------------------- */

.btn {
  font: inherit; font-size: .88rem; line-height: 1.4;
  padding: 6px 12px; border-radius: 6px; cursor: pointer;
  border: 1px solid var(--line-strong); background: var(--bg); color: var(--fg);
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
}
.btn:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn:disabled:hover { border-color: var(--line-strong); color: var(--fg); }
.btn.primary {
  background: var(--accent); border-color: var(--accent); color: var(--accent-fg); font-weight: 550;
}
.btn.primary:hover { filter: brightness(1.08); color: var(--accent-fg); }
/* Red is reserved for controls that take something away, so a destructive button is
   recognisable before its label is read. */
.btn.danger:hover { border-color: var(--bad); color: var(--bad); }
.btn.small { font-size: .8rem; padding: 3px 8px; }
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }

/* A refresh in flight. Faded rather than hidden or spinner-covered: the rows on screen are still
   the last ones that were true, and a reader mid-sentence should be able to finish it. */
[data-refresh][aria-busy="true"] { opacity: .6; transition: opacity .1s ease-in; }

/* Forms ------------------------------------------------------------------ */

.field { margin-bottom: 14px; }
.field label { display: block; font-size: .85rem; font-weight: 550; margin-bottom: 4px; }
.field .hint { margin-top: 4px; }

input[type=text], input[type=email], input[type=url], input[type=search], input[type=password], input[type=date], select, textarea {
  width: 100%; font: inherit; font-size: .9rem; padding: 7px 9px;
  border: 1px solid var(--line-strong); border-radius: 6px;
  background: var(--bg); color: var(--fg);
}
input:focus-visible, select:focus-visible, textarea:focus-visible, .btn:focus-visible, a:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px;
}
textarea { resize: vertical; }
/* A second form further down a page. A card's own bottom margin does not help once a button row follows
   it, so without this the next card sits flush against the previous form's buttons. */
.form-section { margin-top: 32px; }
/* A whole number of pixels, and the same one on the gutter below. 13px x 1.55 is 20.15px, which the
   browser rounds to 1/64px - up for the textarea's lines and down for the gutter's - so the numbers
   crept up on their lines by a sixty-fourth of a pixel each, and by line 500 were half a line out. */
textarea.script {
  font-family: var(--mono); font-size: 13px; line-height: 20px;
  min-height: 55vh; tab-size: 2; white-space: pre;
}
.script-editor {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  overflow: hidden; border: 1px solid var(--line-strong); border-radius: 6px;
  background: var(--bg);
}
.script-editor textarea.script {
  min-width: 0; margin: 0; border: 0; border-radius: 0; box-sizing: border-box;
}
.script-line-numbers {
  min-width: 2.8em; min-height: 0; margin: 0; padding: 7px 8px 7px 9px;
  overflow: hidden; border-right: 1px solid var(--line); box-sizing: border-box;
  background: var(--panel); color: var(--muted); font-family: var(--mono); font-size: 13px;
  line-height: 20px; text-align: right; user-select: none; white-space: pre;
}
/* Inline, inside the gutter's own text: the numbers are lines of text laid out as the textarea lays out
   its lines, rather than a stack of blocks laid out another way. */
.script-line-number.error { color: var(--bad); background: color-mix(in srgb, var(--bad) 14%, transparent); }

/* Ruby dialplan editor --------------------------------------------------- */

.dialplan-editor {
  min-width: 800px;
}
.dialplan-editor > form { min-width: 0; }
.ruby-dialplan-help-toggle { margin-left: 4px; }
.ruby-dialplan-help-backdrop {
  position: fixed; z-index: 900; inset: 0; border: 0; padding: 0; opacity: 0;
  background: rgb(0 0 0 / .35); transition: opacity .2s ease;
}
.ruby-dialplan-help-backdrop.is-open { opacity: 1; }
.ruby-dialplan-help {
  position: fixed; z-index: 901; top: 0; right: 0; bottom: 0; width: min(540px, 100vw);
  display: flex; flex-direction: column; transform: translateX(100%);
  background: var(--panel); border-left: 1px solid var(--line); box-shadow: -8px 0 28px rgb(0 0 0 / .2);
  transition: transform .2s ease;
}
.ruby-dialplan-help.is-open { transform: translateX(0); }
.ruby-dialplan-help-header {
  display: flex; align-items: center; gap: 12px; padding: 14px 18px;
  border-bottom: 1px solid var(--line); flex: none;
}
.ruby-dialplan-help-header h2 { margin: 0; font-size: 1rem; }
.ruby-dialplan-help-header .btn { margin-left: auto; }
.ruby-dialplan-help-body {
  padding: 4px 18px 22px; overflow: auto; font-size: .88rem;
}
.ruby-dialplan-help-body h2 { font-size: .95rem; margin: 20px 0 7px; }
.ruby-dialplan-help-body p { margin-bottom: .7em; }
.ruby-dialplan-help-body dl { margin: 0; }
.ruby-dialplan-help-body dt { font-weight: 600; margin-top: 9px; }
.ruby-dialplan-help-body dd { margin: 2px 0 0; color: var(--muted); }
.ruby-dialplan-help-body ul { margin: 0; padding-left: 18px; }
.ruby-dialplan-help-body li { margin-bottom: 4px; }
.ruby-dialplan-help-body pre {
  overflow-x: auto; margin: 8px 0 0; padding: 10px; border-radius: 5px;
  background: var(--bg); border: 1px solid var(--line); font-size: .78rem; line-height: 1.45;
}
.ruby-dialplan-help-body code { overflow-wrap: anywhere; }
.ruby-examples code { white-space: normal; }
.ruby-dialplan-validation {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: -10px 0 18px;
}
.ruby-dialplan-validation .good { color: var(--good); }
.ruby-dialplan-validation .bad { color: var(--bad); }
.ruby-dialplan-test-result { margin-top: 12px; }
.ruby-dialplan-test-result p { margin: 0 0 6px; color: var(--muted); }
.ruby-dialplan-test-result ol { margin: 0; padding-left: 22px; }
.ruby-dialplan-test-result li { margin-bottom: 3px; }

/* Simple wizard. The Silverlight wizard coloured its form in bands - match the call, when, then what
   to do - and people found their way around it by them, so the bands stay, tinted from the tokens so
   they hold in the dark theme too. */
.wizard-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--line-strong); margin-bottom: 14px; }
.wizard-tabs a {
  padding: 8px 14px; color: var(--muted); border: 1px solid transparent;
  border-bottom: none; border-radius: 6px 6px 0 0; margin-bottom: -1px;
}
.wizard-tabs a:hover { color: var(--fg); text-decoration: none; }
.wizard-tabs a[aria-current="page"] {
  color: var(--fg); font-weight: 600; background: var(--bg); border-color: var(--line-strong);
}
.wizard-form { margin-top: 16px; }
.wizard-form > summary {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap; cursor: pointer; list-style: none;
  padding: 13px 16px; background: var(--panel);
}
.wizard-form > summary::-webkit-details-marker { display: none; }
.wizard-form > summary::before { content: "▶"; font-size: .7rem; color: var(--muted); }
.wizard-form[open] > summary::before { content: "▼"; }
.wizard-form[open] > summary { border-bottom: 1px solid var(--line); }
.wizard-form > summary h2 { margin: 0; font-size: 1rem; }
.wizard-editing { border-bottom: 1px solid var(--line); padding-top: 10px; padding-bottom: 10px; }
.wizard-test-result { margin-top: 12px; }
.wizard-test-result p { margin: 0 0 6px; }
.wizard-test-result ol { margin: 0; padding-left: 22px; }
.wizard-test-result li { margin-bottom: 3px; }
.wizard-test-result .matched { color: var(--good); font-weight: 600; }
.wizard-test-result .skipped { color: var(--muted); }
.wizard-band { border: 0; border-bottom: 1px solid var(--line); margin: 0; padding: 14px 16px 4px; min-width: 0; }
.wizard-band legend {
  float: left; width: 100%; padding: 0; margin-bottom: 10px;
  font-size: .78rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
}
.wizard-band legend + * { clear: both; }
.wizard-band.match { background: color-mix(in srgb, var(--good) 6%, var(--bg)); }
.wizard-band.time { background: var(--panel-2); }
.wizard-band.process { background: color-mix(in srgb, var(--accent) 6%, var(--bg)); }
.wizard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0 16px; align-items: start; }
.wizard-grid .wide { grid-column: span 2; }
.wizard-band .narrow { max-width: 140px; }
.wizard-check { margin-top: 26px; }
.wizard-radios, .wizard-days { gap: 14px; margin-bottom: 12px; }
.wizard-radios .check, .wizard-days .check { margin: 0; align-items: center; font-size: .9rem; }
.wizard-radios .check input, .wizard-days .check input { margin: 0; }
.field-label { display: block; font-size: .85rem; font-weight: 550; margin-bottom: 6px; }
input[type=time], input[type=datetime-local] {
  font: inherit; font-size: .9rem; padding: 6px 8px; border: 1px solid var(--line-strong);
  border-radius: 6px; background: var(--bg); color: var(--fg);
}
.wizard-actions { padding: 14px 16px; }
table.data tr.wizard-selected td { background: color-mix(in srgb, var(--accent) 9%, var(--bg)); }
table.data tr.wizard-disabled td { color: var(--muted); }
.wizard-json { margin-top: 18px; }
.wizard-json summary { cursor: pointer; color: var(--muted); font-size: .85rem; }
.wizard-json pre {
  font-family: var(--mono); font-size: 12px; background: var(--panel); border: 1px solid var(--line);
  border-radius: 6px; padding: 10px; overflow: auto; max-height: 40vh;
}
@media (max-width: 640px) {
  .wizard-grid .wide { grid-column: auto; }
}

.check { display: flex; gap: 8px; align-items: flex-start; margin-bottom: 10px; font-size: .9rem; }
.check input { margin: 3px 0 0; flex: none; }
.check .hint { display: block; margin-top: 1px; }

.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 0 18px; }

.inline-form { display: flex; gap: 8px; align-items: flex-start; flex-wrap: wrap; }
.inline-form .field { margin: 0; flex: 1 1 180px; }
.inline-form .btn { margin-top: 22px; }
.inline-form.bare .btn { margin-top: 0; }

/* Notices ---------------------------------------------------------------- */

.notice {
  border: 1px solid var(--line-strong); border-left-width: 3px; border-radius: 6px;
  padding: 10px 14px; margin-bottom: 20px; font-size: .9rem;
}
.notice.good { border-left-color: var(--good); background: var(--good-bg); }
.notice.bad { border-left-color: var(--bad); background: var(--bad-bg); }
.notice.warn { border-left-color: var(--warn); background: var(--warn-bg); }
.notice p:last-child { margin-bottom: 0; }

/* Tables ----------------------------------------------------------------- */

/* The replacement for jqGrid. The old grids were fixed at 960x235 pixels with their own scrollbar
   inside the page's scrollbar, which meant six rows visible on any screen and a horizontal scrollbar
   on a phone. This scrolls horizontally only when it has to, and never traps the page's own scroll. */
.table-wrap { overflow-x: auto; }

table.data { width: 100%; border-collapse: collapse; font-size: .88rem; }
table.data th, table.data td {
  text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
table.data thead th {
  /* top:0, not the header's height. overflow-x:auto on .table-wrap makes it the nearest scrolling
     ancestor, so `top` is measured from the top of the wrap rather than from the viewport - an offset
     here pushes the header down over the first row instead of clearing the site header. With no
     max-height the wrap never scrolls vertically, so this is inert until one does. */
  position: sticky; top: 0; z-index: 2;
  background: var(--panel); color: var(--muted); font-weight: 600;
  font-size: .78rem; text-transform: uppercase; letter-spacing: .04em;
  white-space: nowrap; border-bottom: 1px solid var(--line-strong);
}
table.data tbody tr:hover { background: var(--panel); }
table.data tbody tr:last-child td { border-bottom: none; }
table.data td.actions { text-align: right; white-space: nowrap; }
/* The one row that must not wrap. Three buttons stacked vertically triples the row's height and
   makes the column read as three separate rows; the table scrolls sideways instead. */
table.data td.actions .btn-row { flex-wrap: nowrap; justify-content: flex-end; }
table.data td.num { text-align: right; font-variant-numeric: tabular-nums; }
table.data .primary-cell { font-weight: 550; }
table.data .sub { color: var(--muted); font-size: .82rem; }
/* A sub line carrying a reason rather than a detail - a provider's registration failure, or an
   expiry the far end cut down. Both sit under a value that looks unremarkable on its own, so
   the colour is what separates "here is more information" from "here is what is wrong". */
table.data .sub.bad { color: var(--bad); }
table.data .sub.warn { color: var(--warn); }
/* Provider errors are whole sentences from a SIP stack and can be long. Wrapping inside the
   cell keeps them readable without the table growing a horizontal scrollbar for one row. */
table.data .sub.bad { max-width: 42ch; overflow-wrap: anywhere; }
/* A provider note is free text and can be a paragraph. Two lines in the table, the rest on the
   title attribute: enough to recognise the note, never enough to reshape the row. */
table.data .sub.note {
  max-width: 38ch; white-space: pre-line; overflow: hidden;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
}

/* Sortable headers. The button is inside the th so the whole cell is not a click target - the
   header still has to be selectable text. */
th.sortable button {
  all: unset; cursor: pointer; display: inline-flex; align-items: center; gap: 4px;
  font: inherit; color: inherit; text-transform: inherit; letter-spacing: inherit;
}
th.sortable button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
th.sortable button::after { content: "↕"; opacity: .3; font-size: .9em; }
th.sortable[aria-sort="ascending"] button::after { content: "↑"; opacity: 1; color: var(--accent); }
th.sortable[aria-sort="descending"] button::after { content: "↓"; opacity: 1; color: var(--accent); }

.table-filter { position: relative; flex: 0 1 260px; }
.table-filter input { padding-left: 28px; font-size: .85rem; }
.table-filter::before {
  content: "⌕"; position: absolute; left: 9px; top: 50%; transform: translateY(-50%);
  color: var(--muted); pointer-events: none; font-size: 1rem;
}

.empty {
  padding: 32px 16px; text-align: center; color: var(--muted); font-size: .9rem;
}
/* Shown by JS when a filter matches nothing, so an empty result reads as "nothing matched"
   rather than as "you have nothing". */
.no-matches { display: none; }
.no-matches.on { display: block; }

/* Paging ----------------------------------------------------------------- */

/* The counterpart of the card header, under the table it belongs to: how many rows at a time on the
   left, where you are in them on the right. Below rather than above because the answer to "is there
   more" is wanted at the point where the rows have run out. */
.table-foot {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 10px 16px; border-top: 1px solid var(--line); background: var(--panel);
}
.page-size { display: flex; align-items: center; gap: 8px; font-size: .82rem; color: var(--muted); }
.page-size select { font-size: .82rem; padding: 3px 6px; width: auto; }

.pager { display: flex; align-items: center; gap: 6px; margin-left: auto; flex-wrap: wrap; }
.pager-state {
  font-size: .82rem; color: var(--muted); padding: 0 6px;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* The ends of the history, rendered as spans rather than links: there is nowhere for them to go, and
   a link that goes nowhere is worse than a control that says it is finished. */
.btn[aria-disabled="true"] { opacity: .45; pointer-events: none; }

/* A header that reorders the whole history rather than the rows on screen, so it is a link to a
   sorted page and not a button that rearranges what is already here. Styled like the in-page sort
   it replaced, because it answers the same question. */
th.sort-link a {
  display: inline-flex; align-items: center; gap: 4px;
  color: inherit; text-decoration: none;
}
th.sort-link a:hover { color: var(--accent); }
th.sort-link a::after { content: "↕"; opacity: .3; font-size: .9em; }
th.sort-link[aria-sort="ascending"] a::after { content: "↑"; opacity: 1; color: var(--accent); }
th.sort-link[aria-sort="descending"] a::after { content: "↓"; opacity: 1; color: var(--accent); }

/* Badges ----------------------------------------------------------------- */

.badge {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: .72rem; font-weight: 600; letter-spacing: .02em;
  padding: 1px 7px; border-radius: 10px; white-space: nowrap;
  border: 1px solid var(--line-strong); color: var(--muted);
}
.badge.good { border-color: var(--good); color: var(--good); background: var(--good-bg); }
.badge.warn { border-color: var(--warn); color: var(--warn); background: var(--warn-bg); }
.badge.bad { border-color: var(--bad); color: var(--bad); background: var(--bad-bg); }
.badge[title] { cursor: help; }
.hint.good { color: var(--good); }
.hint.warn { color: var(--warn); }
.hint.bad { color: var(--bad); }
.validation-problems { margin: 4px 0 0; padding-left: 18px; }
/* A result with a list under it takes its own line below the buttons rather than squeezing between them. */
.btn-row .ruby-dialplan-validation:has(.validation-problems) { display: contents; }
.btn-row .hint:has(.validation-problems) { flex-basis: 100%; order: 10; }
.dialplan-problems ul { margin: 4px 0 0; padding-left: 18px; }
.badge .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }

.uri {
  font-family: var(--mono); font-size: .8rem; overflow-wrap: anywhere;
  display: inline-block; max-width: 42ch;
}
/* A URI cut to one line with an ellipsis, for a column that would otherwise take the table's width.
   The element carries the whole value in its title, and wherever it is shortened the full value is
   one click away. */
.uri.truncate {
  display: block; max-width: 30ch; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; overflow-wrap: normal;
}

/* Dialogs ---------------------------------------------------------------- */

/* A native <dialog>, which is what jqGrid's modal was reimplementing in jQuery UI: focus is trapped,
   Escape closes it, and the backdrop is the browser's rather than a positioned div. */
dialog.modal {
  border: 1px solid var(--line-strong); border-radius: var(--radius); padding: 0;
  background: var(--bg); color: var(--fg); box-shadow: var(--shadow);
  width: min(680px, 94vw); max-height: 88vh;
}
dialog.modal::backdrop { background: rgba(15, 23, 42, .45); }
dialog.modal header {
  display: flex; align-items: baseline; gap: 10px;
  padding: 14px 18px; border-bottom: 1px solid var(--line); background: var(--panel);
}
dialog.modal header h2 { margin: 0; font-size: 1rem; }
dialog.modal header .subject { color: var(--muted); font-size: .85rem; font-family: var(--mono); }
dialog.modal header .close { margin-left: auto; }
dialog.modal .body { padding: 18px; overflow-y: auto; max-height: calc(88vh - 124px); }
dialog.modal footer {
  display: flex; gap: 8px; justify-content: flex-end;
  padding: 12px 18px; border-top: 1px solid var(--line); background: var(--panel);
}

/* Secrets ---------------------------------------------------------------- */

/* A password, shown once after it is generated or on request. Set apart from ordinary text so it is
   obvious that something on screen wants copying now rather than reading later. */
.secret {
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  font-family: var(--mono); font-size: 1rem;
  background: var(--panel-2); border: 1px dashed var(--line-strong);
  border-radius: 6px; padding: 10px 12px; margin: 10px 0;
  user-select: all; overflow-wrap: anywhere;
}
.secret .btn { user-select: none; font-family: ui-sans-serif, system-ui, sans-serif; }

/* Home ------------------------------------------------------------------- */

.hero { padding: 56px 0 36px; border-bottom: 1px solid var(--line); }
.hero h1 { font-size: clamp(1.9rem, 4.2vw, 2.7rem); max-width: 20ch; }
.hero p.lead { font-size: 1.1rem; color: var(--muted); max-width: 62ch; }
.hero .btn-row { margin-top: 22px; }

/* The migration announcement under the hero: a notice in weight, a page in length, so it reads as prose. */
.announcement {
  margin: 32px 0 8px; padding: 22px 26px; max-width: 78ch;
  border: 1px solid var(--line); border-left: 4px solid var(--warn); border-radius: 6px;
  background: var(--warn-bg);
}
.announcement h2 { margin-top: 0; }
.announcement h3 { margin: 1.4em 0 .4em; font-size: 1.02rem; }
.announcement p.lead { color: var(--muted); }
.announcement ul { padding-left: 1.2em; }
.announcement li { margin-bottom: .45em; }

.features { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 20px; }
.feature h3 { margin-bottom: .3em; }
.feature p { color: var(--muted); font-size: .92rem; margin: 0; }

.faq dt { font-weight: 600; margin-top: 1.4em; }
.faq dd { margin: .3em 0 0; color: var(--muted); }

.kv { display: grid; grid-template-columns: auto 1fr; gap: 6px 18px; font-size: .9rem; }
.kv dt { color: var(--muted); }
.kv dd { margin: 0; }

/* Dial plan trace on a call's details. Messages are sentences, often with a pattern or a dial string
   in them, so they wrap rather than stretch the table. */
table.data.trace td.trace-message { overflow-wrap: anywhere; max-width: 70ch; }
