/* ─────────────────────────────────────────────────────────────────────────────
   Tokens

   Dark-first, because this panel sits alongside a terminal more often than not.
   Light is a full token swap rather than a set of overrides, so nothing has to be
   redefined further down the file.
   ───────────────────────────────────────────────────────────────────────────── */
:root {
  --bg: #0e1014;
  --panel: #16191f;
  --panel-2: #1c2027;
  --panel-3: #22262e;
  --line: #282d36;
  --line-strong: #363c47;
  --text: #e8eaee;
  --muted: #98a1b0;
  --accent: #5b93ff;
  --accent-soft: color-mix(in srgb, var(--accent) 16%, transparent);
  --ok: #3ecf8e;
  --warn: #e2b33c;
  --err: #f2666b;

  /* Spacing scale — every margin and padding below comes from here. */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;
  --s7: 48px;

  --radius: 10px;
  --radius-sm: 6px;
  --shadow: 0 1px 2px rgb(0 0 0 / 0.28), 0 8px 24px rgb(0 0 0 / 0.18);
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #f5f6f8;
    --panel: #ffffff;
    --panel-2: #f4f6f9;
    --panel-3: #eaeef3;
    --line: #e2e6ec;
    --line-strong: #cfd6e0;
    --text: #171a1f;
    --muted: #616b7a;
    --accent: #2f6fe4;
    --shadow: 0 1px 2px rgb(16 24 40 / 0.06), 0 8px 24px rgb(16 24 40 / 0.06);
  }
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* Alpine sets this until it boots, so templated content never flashes unstyled.
   !important is required: it has to beat the display value the element would take. */
[x-cloak] {
  /* biome-ignore lint/complexity/noImportantStyles: must override the element's own display */
  display: none !important;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font:
    14px / 1.55 ui-sans-serif,
    system-ui,
    -apple-system,
    "Segoe UI",
    sans-serif;
  -webkit-font-smoothing: antialiased;
}

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

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

/* ───────────────────────────────────────────────────────────── header + nav */

header {
  border-bottom: 1px solid var(--line);
  background: color-mix(in srgb, var(--panel) 88%, transparent);
  backdrop-filter: blur(8px);
  position: sticky;
  top: 0;
  z-index: 20;
}
/* Inner wrapper, so the chrome sits on the same column as `main`'s content. */
header .bar {
  max-width: 1080px;
  margin: 0 auto;
  padding: 0 var(--s5);
  display: flex;
  align-items: center;
  gap: var(--s5);
  height: 54px;
}
header .brand {
  font-weight: 650;
  letter-spacing: -0.01em;
  color: var(--text);
  white-space: nowrap;
}
header .brand:hover {
  text-decoration: none;
  color: var(--accent);
}
/* The app's sections. Only this half scrolls when the row is too narrow. */
header nav {
  display: flex;
  align-items: center;
  gap: var(--s1);
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
header nav::-webkit-scrollbar {
  display: none;
}
/* Password and Sign out are account actions, not sections — pushed right, behind a rule,
   and deliberately outside `nav` so they can never scroll out of reach. */
header .account {
  display: flex;
  align-items: center;
  gap: var(--s1);
  margin-left: auto;
  padding-left: var(--s3);
  border-left: 1px solid var(--line);
  flex: none;
}
header nav a,
header .account a {
  color: var(--muted);
  font-weight: 500;
  padding: var(--s1) var(--s3);
  border-radius: var(--radius-sm);
  white-space: nowrap;
  transition:
    color 0.12s ease,
    background 0.12s ease;
}
header nav a:hover,
header .account a:hover {
  color: var(--text);
  background: var(--panel-2);
  text-decoration: none;
}
header nav a.active,
header .account a.active {
  color: var(--text);
  background: var(--panel-3);
}

/* Sign-out is a POST form so a prefetch cannot trigger it; `display: contents` lets the
   button sit in the flex row as if the form were not there. The button's own look is set
   in the buttons section below, which must come after the generic `button` rule. */
header .account form {
  display: contents;
}

/* A single-column page — login, password — inside the wide `main`. */
.narrow {
  max-width: 420px;
  margin: var(--s7) auto 0;
}

main {
  max-width: 1080px;
  margin: 0 auto;
  padding: var(--s5) var(--s5) var(--s7);
}

/* ──────────────────────────────────────────────────────────────── typography */

h1 {
  font-size: 22px;
  margin: 0 0 var(--s1);
  letter-spacing: -0.02em;
  font-weight: 640;
}
h2 {
  font-size: 14px;
  margin: 0;
  letter-spacing: 0.01em;
  font-weight: 620;
}
.sub {
  color: var(--muted);
  margin: 0 0 var(--s5);
  max-width: 68ch;
}
.hint {
  color: var(--muted);
  font-size: 12.5px;
  margin-top: var(--s1);
  font-weight: 400;
}
.hint.tight {
  margin: 0;
}
/* A hint that leads a card, above the fields it applies to. */
.hint.lead {
  margin: 0 0 var(--s4);
}
.muted {
  color: var(--muted);
}
.bad {
  color: var(--err);
  font-weight: 600;
}
.right {
  text-align: right;
}
.nowrap {
  white-space: nowrap;
}
.mono,
td.mono {
  font-family: var(--mono);
  font-size: 12.5px;
}
code {
  font-family: var(--mono);
  font-size: 12.5px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 1px 5px;
}

/* A title row and its actions, sharing a baseline. */
.page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s4);
  margin-bottom: var(--s5);
  flex-wrap: wrap;
}
.page-head .sub {
  margin-bottom: 0;
}
/* A heading with no `.sub` beneath it; the wrapper already carries the gap below. */
.page-head h1:only-child {
  margin-bottom: 0;
}

/* ─────────────────────────────────────────────────────────────────── cards */

.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--s4) var(--s5) var(--s5);
  margin-bottom: var(--s4);
}
.card.subtle {
  background: transparent;
}
.card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  flex-wrap: wrap;
  border-bottom: 1px solid var(--line);
  padding-bottom: var(--s3);
  margin-bottom: var(--s4);
}
.card-head h2 {
  border: none;
  padding: 0;
  margin: 0;
}

.empty {
  color: var(--muted);
  margin: 0;
  padding: var(--s5) 0;
  text-align: center;
}

/* ──────────────────────────────────────────────────────────────────── forms */

label {
  display: block;
  font-weight: 550;
  margin-bottom: var(--s1);
  font-size: 13px;
}
.field {
  margin-bottom: var(--s4);
}
.field:last-child {
  margin-bottom: 0;
}

input[type="text"],
input[type="password"],
input[type="number"],
input[type="search"],
select,
textarea {
  width: 100%;
  padding: 8px 10px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
  transition:
    border-color 0.12s ease,
    background 0.12s ease;
}
input:hover:not(:disabled),
select:hover:not(:disabled),
textarea:hover {
  border-color: var(--line-strong);
}
input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
input:disabled,
select:disabled,
textarea:disabled {
  opacity: 0.6;
  cursor: not-allowed;
  background: var(--panel-3);
}
input.invalid {
  border-color: var(--err);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--err) 16%, transparent);
}
textarea {
  font-family: var(--mono);
  font-size: 12.5px;
  min-height: 68px;
  resize: vertical;
}

/* A checkbox that reads as one line with its text. */
.inline-check {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  margin: 0;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
}
.inline-check input[type="checkbox"] {
  width: auto;
  margin: 0;
  accent-color: var(--accent);
}

/* ────────────────────────────────────────────────────────────────── buttons */

button,
.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: 8px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  color: var(--text);
  font: inherit;
  font-weight: 550;
  cursor: pointer;
  white-space: nowrap;
  transition:
    border-color 0.12s ease,
    background 0.12s ease,
    transform 0.06s ease;
}
button:hover:not(:disabled),
.btn:hover {
  border-color: var(--line-strong);
  background: var(--panel-3);
  text-decoration: none;
}
button:active:not(:disabled) {
  transform: translateY(1px);
}
button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
button.primary:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 86%, #000);
  border-color: color-mix(in srgb, var(--accent) 86%, #000);
}
button.danger {
  color: var(--err);
  border-color: color-mix(in srgb, var(--err) 45%, transparent);
}
button.danger:hover:not(:disabled) {
  background: color-mix(in srgb, var(--err) 12%, transparent);
  border-color: var(--err);
}
button.small,
.btn.small {
  padding: 5px 10px;
  font-size: 12.5px;
}
button:disabled,
button[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
}
.actions {
  display: flex;
  gap: var(--s2);
  flex-wrap: wrap;
  align-items: center;
}
/* The sign-out button, styled to match the links beside it. Defined here rather than in
   the header section so it follows the generic `button` rule above. */
header .account button {
  color: var(--muted);
  font-weight: 500;
  padding: var(--s1) var(--s3);
  border: 0;
  background: none;
  box-shadow: none;
  white-space: nowrap;
}
header .account button:hover {
  color: var(--text);
  background: var(--panel-2);
  border: 0;
}

/* Page links under a table. A disabled edge renders as a span, so it is not focusable. */
.pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s3);
  margin-top: var(--s4);
  padding-top: var(--s4);
  border-top: 1px solid var(--line);
}
.pager [aria-disabled="true"] {
  opacity: 0.45;
  pointer-events: none;
}

/* The error page's links, below the detail block. */
.error-actions {
  margin-top: var(--s4);
}

.save-bar {
  display: flex;
  align-items: center;
  gap: var(--s3);
}

/* ─────────────────────────────────────────────────────────────────── tables */

/* Wide tables scroll inside the card rather than the page. */
.table-scroll {
  overflow-x: auto;
  margin: 0 calc(var(--s5) * -1);
  padding: 0 var(--s5);
}
table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}
th,
td {
  text-align: left;
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
th {
  /* Deliberately not sticky: these tables live inside an `overflow-x` scroll
     container, where a sticky header resolves against that container and ends up
     painted over the first row. The tables are short enough not to need it. */
  background: var(--panel);
  color: var(--muted);
  font-weight: 600;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
tbody tr:last-child td {
  border-bottom: none;
}
tbody tr:hover {
  background: var(--panel-2);
}

/* A checkbox column: no header text, and no right padding — the colgroup sets its width. */
.col-check {
  padding-right: 0;
}

/* Eleven columns. Below this the cells crush — "full · dry run" wraps to three lines —
   so `.table-scroll` takes over instead, the same bargain `table.fields` makes. */
table.runs {
  min-width: 860px;
}

/*
 * The two field-mapping tables. `fixed` is load-bearing: with auto layout the Notion
 * property column is sized to the em dash an unmapped row shows, so ticking one row —
 * which replaces that dash with a select and a text input — reflows every column on the
 * page. Fixed widths mean the layout is the same before and after, which is what you
 * want when nearly every row ends up mapped.
 *
 * min-width keeps the columns usable on a narrow screen; .table-scroll takes over from
 * there rather than letting the cells crush.
 */
table.fields {
  table-layout: fixed;
  min-width: 820px;
}
table.fields .c-check {
  width: 34px;
}
/* Wide enough for `encryptedstring`, the longest type offered. */
table.fields .c-sftype {
  width: 132px;
}
/* The cell, not the badge: `.type-tag` styles inline chips inside other cells. */
td.col-sftype {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--muted);
  white-space: nowrap;
}
/* Wide from the start: this holds the target select beside the property-name input. */
table.fields .c-target {
  width: 40%;
}
table.fields .c-as {
  width: 140px;
}
/* The Notion type and its badges. Wraps to a second line for the widest case — a mapped
   relation — so the column can stay narrow instead of reserving room it rarely needs.
   This must be an element *inside* the cell, never the `td`: a flex `td` stops taking the
   row's height, and its bottom border then renders offset from every other column's. */
.as-cell {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s1) var(--s2);
}

/* Holds its space in the layout while hidden, so nothing beside it moves. */
.invisible {
  visibility: hidden;
}

/* Visually hidden but still read out — used for the checkbox column's header. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ────────────────────────────────────────────────────────── pills + banners */

.pill {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 99px;
  font-size: 11.5px;
  font-weight: 600;
  border: 1px solid var(--line);
  background: var(--panel-2);
  color: var(--muted);
  white-space: nowrap;
}
.pill.ok {
  color: var(--ok);
  border-color: color-mix(in srgb, var(--ok) 40%, transparent);
  background: color-mix(in srgb, var(--ok) 10%, transparent);
}
.pill.err {
  color: var(--err);
  border-color: color-mix(in srgb, var(--err) 40%, transparent);
  background: color-mix(in srgb, var(--err) 10%, transparent);
}
.pill.warn {
  color: var(--warn);
  border-color: color-mix(in srgb, var(--warn) 40%, transparent);
  background: color-mix(in srgb, var(--warn) 10%, transparent);
}
.pill.new,
.pill.env {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
  background: var(--accent-soft);
}
.pill.muted-pill {
  color: var(--muted);
}

.banner {
  padding: var(--s3) var(--s4);
  border-radius: var(--radius-sm);
  margin-bottom: var(--s4);
  border: 1px solid var(--line);
}
.banner.err {
  border-color: color-mix(in srgb, var(--err) 45%, transparent);
  background: color-mix(in srgb, var(--err) 10%, transparent);
}
.banner.warn {
  border-color: color-mix(in srgb, var(--warn) 45%, transparent);
  background: color-mix(in srgb, var(--warn) 10%, transparent);
}
/* A banner below other card content, rather than leading it. */
.banner.inset {
  margin-top: var(--s4);
  margin-bottom: 0;
}
.banner.info {
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  background: var(--accent-soft);
}

/* ────────────────────────────────────────────────────────────── login: root */

/* The login page's "Root admin login" disclosure. A <details> rather than an Alpine
   toggle, because this is the break-glass login and it has to work with no JavaScript at
   all. The default summary renders as a bare list-item, so it gets a control's
   affordances here. No margin-top: adjacent margins collapse against the preceding
   card's margin-bottom. */
.login-alt > summary {
  cursor: pointer;
  list-style: none;
  color: var(--muted);
  font-weight: 550;
  font-size: 13px;
  padding: var(--s1) 0;
}
.login-alt > summary::-webkit-details-marker {
  display: none;
}
.login-alt > summary::before {
  content: "\25B8";
  display: inline-block;
  width: 1em;
  transition: transform 0.12s ease;
}
.login-alt[open] > summary::before {
  transform: rotate(90deg);
}
.login-alt > summary:hover,
.login-alt > summary:focus-visible {
  color: var(--text);
}
.login-alt[open] > summary {
  margin-bottom: var(--s3);
}

/* ─────────────────────────────────────────────────────────────────── toasts */

.toasts {
  position: fixed;
  bottom: var(--s5);
  right: var(--s5);
  z-index: 50;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  max-width: min(420px, calc(100vw - var(--s6)));
}
.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong);
  background: var(--panel);
  box-shadow: var(--shadow);
  font-size: 13px;
}
.toast.ok {
  border-color: color-mix(in srgb, var(--ok) 50%, transparent);
}
.toast.err {
  border-color: color-mix(in srgb, var(--err) 50%, transparent);
}
.toast.warn {
  border-color: color-mix(in srgb, var(--warn) 50%, transparent);
}
.toast-close {
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  flex: none;
  border: none;
  background: none;
  color: var(--muted);
  padding: 0;
  font-size: 18px;
  line-height: 20px;
  cursor: pointer;
}
.toast-close:hover {
  color: var(--text);
  background: none;
}

/* ───────────────────────────────────────────────────────────────── progress */

.progress {
  height: 8px;
  border-radius: 99px;
  background: var(--panel-3);
  overflow: hidden;
  margin-bottom: var(--s2);
}
.progress-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 99px;
  transition: width 0.4s ease;
}
/* Before the record count lands there is no percentage to show, so it sweeps. */
.progress.indeterminate .progress-fill {
  width: 35%;
  animation: sweep 1.3s ease-in-out infinite;
}
@keyframes sweep {
  0% {
    margin-left: -35%;
  }
  100% {
    margin-left: 100%;
  }
}
.progress-label {
  display: flex;
  justify-content: space-between;
  font-size: 12.5px;
  color: var(--muted);
}

.run-live-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s4);
  margin-bottom: var(--s4);
}
/* The run tallies, from views/partials/_run-counters.ejs. One class for every place
   they appear; `.inset` adds the rule for when they sit below other card content. */
.run-counters {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
  gap: var(--s3);
}
.run-counters.inset {
  margin-top: var(--s4);
  padding-top: var(--s4);
  border-top: 1px solid var(--line);
}
.run-counters .counter-label {
  display: block;
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 500;
}
.run-counters .counter-value {
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.02em;
}

.running-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  border-radius: 99px;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  background: var(--accent-soft);
  font-size: 12.5px;
}
.running-chip .dot {
  width: 7px;
  height: 7px;
  border-radius: 99px;
  background: var(--accent);
  animation: pulse 1.4s ease-in-out infinite;
}
@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.25;
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    /* biome-ignore lint/complexity/noImportantStyles: a motion opt-out must beat every rule */
    animation: none !important;
    /* biome-ignore lint/complexity/noImportantStyles: a motion opt-out must beat every rule */
    transition: none !important;
  }
}

.tabs {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  border: 1px solid var(--line);
}
.tabs button {
  border: none;
  background: none;
  color: var(--muted);
  padding: 5px 12px;
  font-size: 12.5px;
}
.tabs button:hover:not(.active) {
  background: none;
  color: var(--text);
}
.tabs button.active {
  background: var(--panel);
  color: var(--text);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.12);
}

/* ─────────────────────────────────────────────────────────── field mapping */

.field-name {
  font-weight: 550;
}
/* The Notion target: a "create new or link to existing" select beside the property name.
   Lives in a table cell, so it must not wrap the two controls onto separate lines. */
.map-target {
  display: flex;
  gap: var(--s2);
  flex-wrap: nowrap;
}

/* Mapped rows get an accent edge, so they can be found by eye in ~150 rows without
   reading every checkbox. `.row-off` is set on the unmapped ones. */
tbody tr:not(.row-off) .col-check {
  box-shadow: inset 2px 0 var(--accent);
}
.map-target select {
  flex: 1 1 52%;
  min-width: 0;
}
.map-target input {
  flex: 1 1 48%;
  min-width: 0;
}
.type-tag {
  font-size: 11.5px;
  color: var(--muted);
  font-family: var(--mono);
}
.title-card {
  border-color: color-mix(in srgb, var(--accent) 30%, var(--line));
}

.filters {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
}
.filters .search {
  width: auto;
  min-width: 200px;
}
/* Controls in the filter bar size to themselves; the global rule stretches them to 100%. */
.filters select {
  width: auto;
}
/* The runs table's range line, above the table it describes. */
.count-summary {
  margin: 0 0 var(--s3);
}
.count-line {
  margin-top: var(--s2);
  font-size: 12.5px;
  color: var(--muted);
}

/* ────────────────────────────────────────────────────────────── objects page */

/* The three database pickers stack as one condensed block, so they read as a single
   set-up step rather than three independent settings — which is what they are: a
   lookup only becomes a relation if its target object already has a database. */
.db-rows {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  /* What the last `.field` contributes above `.actions` on the object cards below. */
  margin-bottom: var(--s4);
}
.db-row {
  display: grid;
  grid-template-columns: 6.5rem 1fr;
  align-items: center;
  gap: var(--s3);
}
.db-name {
  font-weight: 550;
}
/* The flex gap alone spaces these; the banner's own bottom margin would double it. */
.db-rows > .banner {
  margin-bottom: 0;
}
.db-control {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
}
/* Every control in a row is pinned to one height: a select and a text input resolve to
   different intrinsic heights, and the two panels swap places, so any difference shows up
   as the rows below jumping. */
.db-control select,
.db-control input[type="text"] {
  min-width: 0;
  height: 37px;
  padding-top: 0;
  padding-bottom: 0;
}
/* The picker stops growing well before it becomes an empty bar. The create panel's two
   controls share what is left instead — capping them the same way made the row wrap to
   two lines, since `flex-wrap` wraps before it shrinks. */
.db-pick {
  flex: 0 1 26rem;
}
.db-new {
  flex: 1 1 9rem;
}
/* Linked to from the dashboard; the header is sticky, so clear it. */
.object-card {
  scroll-margin-top: 5rem;
}

@media (max-width: 560px) {
  .db-row {
    grid-template-columns: 1fr;
    gap: var(--s2);
  }
  /* The track is too narrow for the cap: let the picker shrink so its button stays on
     the same line, and let the create panel stack rather than clip its controls. */
  .db-pick {
    flex: 1 1 8rem;
  }
  .db-new {
    flex: 1 1 100%;
  }
}

/* ────────────────────────────────────────────────────────────────────── log */

pre.log {
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: var(--s3);
  overflow: auto;
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.65;
  max-height: 60vh;
  white-space: pre-wrap;
  word-break: break-word;
  margin: 0;
}

/* Always laid out, so starting a request cannot change the button's width. */
.spinner {
  visibility: hidden;
  color: var(--muted);
  font-size: 12px;
}
.spinner.on {
  visibility: visible;
}

/* ───────────────────────────────────────────────────────────────── responsive */

@media (max-width: 720px) {
  main {
    padding: var(--s4) var(--s3) var(--s6);
  }
  header .bar {
    padding: 0 var(--s3);
    gap: var(--s3);
  }
  /* Only here can the sections row overflow, so only here does the edge fade. */
  header nav {
    mask-image: linear-gradient(to right, #000 calc(100% - 16px), transparent);
  }
  .card {
    padding: var(--s3) var(--s4) var(--s4);
  }
  .table-scroll {
    margin: 0 calc(var(--s4) * -1);
    padding: 0 var(--s4);
  }
  /* The target select and its name input stack; five columns of controls do not fit a
     phone, and the table scrolls sideways inside its card for the rest. */
  .map-target {
    flex-direction: column;
  }
  .toasts {
    left: var(--s3);
    right: var(--s3);
    bottom: var(--s3);
    max-width: none;
  }
}
