:root {
  --bg: #fafafa;
  /* A card or a box of settings, set off from the page it sits on: white on
     the light page, a step lighter than the dark one. */
  --surface: #ffffff;
  --fg: #1a1a1a;
  --muted: #6b6b6b;
  --accent: #0a6ba3; /* the blue of the icon in the browser tab */
  --border: #e2e2e2;
  --radius: 6px;
  /* What a button of that colour writes on itself. */
  --on-accent: #ffffff;
  --row-hover: color-mix(in srgb, var(--fg) 4%, var(--bg));
  --subtle: color-mix(in srgb, var(--fg) 5%, var(--bg));
  --nav-active: color-mix(in srgb, var(--accent) 12%, var(--bg));
  /* The tints that mean something -- a state, a kind of feed -- and the one
     button that is somebody else's. They are tokens rather than colours in the
     rules because each has a dark twin, and a token is the only way to write
     the twin once instead of writing every rule twice. */
  --ok-bg: #d9f0e1;
  --ok-fg: #1b6b37;
  --bad-bg: #fadcdc;
  --bad-fg: #a92222;
  --amber-bg: #fbf0d9;
  --amber-fg: #875500;
  /* Something to be done, not something wrong: amber, and its dark twin
     with it. */
  --pending-bg: var(--amber-bg);
  --pending-fg: var(--amber-fg);
  /* The number beside the dashboard in the menu: bright enough to be seen on
     either background, so it has no dark twin. */
  --attention-dot: #f2b705;
  --google-bg: #ffffff;
  --google-fg: #1f1f1f;
  --google-border: #747775;
  font-family: system-ui, -apple-system, sans-serif;
  color-scheme: light dark;
}

/* Dark, and who asks for it. The system's own preference is followed unless
   somebody has said otherwise on their profile, and what they said is on the
   html element before the page is drawn -- so there is no flash of the wrong
   one and no script deciding it.
   The list is written twice because CSS has no way to say "this selector or
   that one" across a media query. Everything else reads the tokens. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #161616;
    --surface: #1e1e1e;
    --fg: #ececec;
    --muted: #9a9a9a;
    /* The same blue, lightened: the icon's own is too dark to read here. */
    --accent: #3f9ad6;
    --border: #2e2e2e;
    /* Dark text on the lighter blue: white on it reads at 3.1, this at 6.8. */
    --on-accent: #0b1b24;
    /* The same two colours, dark enough to sit on the page and light enough to
       read on. Paused follows the border and the text, which already turn. */
    --ok-bg: #17371f;
    --ok-fg: #7fd39b;
    --bad-bg: #3b1d1d;
    --bad-fg: #f0a0a0;
    /* The feed formats turn the same way: the hue stays, the lightness swaps. */
    --amber-bg: #3a2e13;
    --amber-fg: #e0bc74;
    --google-bg: #131314;
    --google-fg: #e3e3e3;
    --google-border: #8e918f;
  }
}

:root[data-theme="dark"] {
  --bg: #161616;
  --surface: #1e1e1e;
  --fg: #ececec;
  --muted: #9a9a9a;
  --accent: #3f9ad6;
  --border: #2e2e2e;
  --on-accent: #0b1b24;
  --ok-bg: #17371f;
  --ok-fg: #7fd39b;
  --bad-bg: #3b1d1d;
  --bad-fg: #f0a0a0;
  --amber-bg: #3a2e13;
  --amber-fg: #e0bc74;
  --google-bg: #131314;
  --google-fg: #e3e3e3;
  --google-border: #8e918f;
}

/* What a browser draws itself -- form controls, scrollbars, the space behind
   the page -- follows the choice too, or it is a dark page with white edges. */
:root[data-theme="dark"] { color-scheme: dark; }
:root[data-theme="light"] { color-scheme: light; }

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  line-height: 1.5;
}

a { color: var(--accent); }

.muted { color: var(--muted); }
/* Text for screen readers only. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.error { color: #d33; }

/* A button marked hidden stays hidden: the display below would otherwise
   outrank the browser's own rule for hidden, and show it. */
button[hidden] { display: none; }

button, .button {
  display: inline-block;
  padding: 0.5rem 1rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--on-accent);
  font: inherit;
  text-decoration: none;
  cursor: pointer;
}


.icon-button {
  display: inline-grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
.icon-button:hover { background: var(--border); color: var(--fg); }
.icon-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.icon-button svg { width: 1.25rem; height: 1.25rem; }
td .icon-button:hover { color: #d33; }

.login { max-width: 20rem; margin: 6rem auto; padding: 1rem; text-align: center; }
.login h1 { font-size: 1.5rem; }
/* A card dialog holds both kinds of card's questions, and shows the ones
   belonging to the kind that is chosen. The browser decides it, so choosing a
   kind costs nothing and asks nobody. */
.card-form .for-kpi, .card-form .for-favorite { display: none; }
.card-form:has(#field-kind-kpi:checked) .for-kpi,
.card-form:has(#field-kind-favorite:checked) .for-favorite { display: revert; }
/* A dialog for a card that is being changed has one kind's questions and no
   choice above them, so they are simply there. */
.card-form:not(:has(.choice)) .for-kpi,
.card-form:not(:has(.choice)) .for-favorite { display: revert; }

/* Google's own button: their mark, on white, with the word beside it. Their
   guidelines are particular about it, and the app's blue behind the mark would
   be neither their button nor ours. Dark mode has a dark variant of its own. */
.button.google {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  width: 100%;
  background: var(--google-bg);
  color: var(--google-fg);
  border-color: var(--google-border);
}
.button.google svg { width: 1.25rem; height: 1.25rem; }

/* The two ways in, one under the other, with the second offered more quietly
   than the first. */
.login .form { margin-top: 1rem; text-align: left; }
.login .form button { width: 100%; }
.login .or { position: relative; margin: 1.25rem 0 0; font-size: 0.875rem; }
.login .or::before,
.login .or::after {
  content: "";
  position: absolute;
  top: 50%;
  width: calc(50% - 1.5rem);
  border-top: 1px solid var(--border);
}
.login .or::before { left: 0; }
.login .or::after { right: 0; }
/* A code is read off a screen and typed back, so it is set wide and plain. */
.code-field {
  font-size: 1.25rem;
  letter-spacing: 0.25em;
  font-variant-numeric: tabular-nums;
}
/* A button that is really a way back, not a thing to do. */
button.link {
  padding: 0;
  border: none;
  background: none;
  color: var(--accent);
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}

/* App layout: fixed left menu, content to the right. Stacks on narrow screens. */

.app {
  display: grid;
  grid-template-columns: 15rem 1fr;
  min-height: 100vh;
}

.sidebar {
  display: flex;
  flex-direction: column;
  padding: 1rem;
  border-right: 1px solid var(--border);
  /* The page scrolls; the menu beside it stays where it is. A screen tall
     rather than as tall as the page, so the user at the foot of it is at the
     foot of the window and not somewhere below a long table. It scrolls on its
     own if it is ever taller than the window. */
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}
.sidebar-nav {
  display: flex;
  flex-direction: column;
  flex: 1;
  gap: 1rem;
}

/* The bar the menu folds up into, which only a narrow screen shows. */
.sidebar-toggle { display: none; }
.hamburger { display: flex; }
.hamburger svg { width: 1.5rem; height: 1.5rem; }

/* The logo, and beside it the button that folds the menu down to its icons. */
.brand {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0 0 0 0.5rem;
}
.brand-full svg { display: block; width: 6rem; height: auto; color: var(--accent); }
.brand-mark { display: none; }
.brand-mark svg { display: block; width: 1.75rem; height: 1.75rem; color: var(--accent); }
.sidebar-collapse { margin: 0; }
.login h1 svg { display: block; width: 8rem; height: auto; margin: 0 auto 0.5rem; color: var(--accent); }

.content { padding: 1.5rem 2rem; min-width: 0; }
.content h1 { margin: 0 0 1rem; font-size: 1.5rem; }

.menu { list-style: none; margin: 0; padding: 0; }
.menu a {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.4rem 0.5rem;
  border-radius: var(--radius);
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
}
.menu-icon { display: flex; flex: none; }
.menu-icon svg { width: 1.25rem; height: 1.25rem; }
.menu a:hover { background: var(--row-hover); }
.menu a.active { background: var(--nav-active); color: var(--accent); font-weight: 600; }
/* How many things on the dashboard are to be done: a number at the end of its
   line, or on its icon where the menu is folded. */
.menu a { position: relative; }
.menu-count {
  flex: none; margin-left: auto; min-width: 1.1rem; height: 1.1rem; padding: 0 0.3rem;
  border-radius: 999px; background: var(--attention-dot); color: #3a2c00;
  font-size: 0.7rem; font-weight: 700; line-height: 1.1rem; text-align: center;
}

.account {
  margin-top: auto;
  padding: 0 0.5rem;
  font-size: 0.875rem;
}
.field p { margin: 0; }
/* A line under a field saying what may go in it. */
.field-hint { margin: -0.75rem 0 1rem; }

.team-picker { position: relative; }
.team-picker summary {
  list-style: none;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.team-picker summary::-webkit-details-marker { display: none; }
.team-picker summary::after { content: "▾"; float: right; color: var(--muted); }
.team-picker ul {
  position: absolute;
  z-index: 10;
  left: 0;
  right: 0;
  margin: 0.25rem 0 0;
  /* Somebody in twenty teams gets a list longer than the window, and the menu
     it hangs in scrolls rather than spills. */
  max-height: 60vh;
  overflow-y: auto;
  padding: 0.25rem;
  list-style: none;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
}
.team-picker a, .team-picker button {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
  padding: 0.4rem 0.5rem;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.team-picker a:hover, .team-picker button:hover { background: var(--border); }
.team-picker a.active { font-weight: 600; }
/* Making a team is not switching to one, so it sits under a line. */
.team-picker .new-team {
  margin-top: 0.25rem;
  padding-top: 0.25rem;
  border-top: 1px solid var(--border);
}

@media (max-width: 40rem) {
  /* Stacked, the two become rows of a grid that is at least a screen tall, and
     rows share the height left over. The menu is to be as tall as what it holds
     -- a bar, folded up -- and the page is to have the rest. */
  .app { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
  /* Stacked, the menu is a bar above the page rather than a column beside it,
     so it is as tall as what it holds and scrolls away with everything else. */
  .sidebar {
    padding: 0.5rem 1rem;
    border-right: 0;
    border-bottom: 1px solid var(--border);
    position: static;
    height: auto;
    overflow: visible;
  }
  .content { padding: 1rem; }

  /* The menu stands on its own screen here, so it folds up into a bar saying
     which page one is on, and the page starts under it. */
  .sidebar-toggle {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.4rem 0;
    border: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    font-weight: 600;
    text-align: left;
    cursor: pointer;
  }
  /* Folded unless it has been opened. Said this way round, rather than
     hiding it once a script has run, because a menu that stands for a moment
     and is then taken away moves the whole page under whoever is reading it. */
  .app:not([data-menu="open"]) .sidebar-nav { display: none; }
  .sidebar-nav { padding-top: 0.5rem; }
}

.user {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.25rem;
  min-width: 0;
}
.user span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
td .user { display: inline-flex; margin: 0; vertical-align: middle; }

a.user {
  margin: 0 -0.5rem;
  padding: 0.3rem 0.5rem;
  border-radius: var(--radius);
  color: inherit;
  text-decoration: none;
}
a.user:hover { background: var(--row-hover); }
a.user.active { background: var(--nav-active); color: var(--accent); font-weight: 600; }

.avatar {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  object-fit: cover;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 0.7rem;
  font-weight: 600;
}

.form { max-width: 28rem; }
.form h2 { margin-top: 0; }
/* One form under another on the same page: the second is a different question
   and stands apart from the first rather than running on from it. */
.form + .form { margin-top: 2.5rem; }

.field { margin-bottom: 1rem; }
.field label, .field .label { display: block; margin-bottom: 0.25rem; font-size: 0.875rem; font-weight: 600; }
.field input,
.field textarea {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: transparent;
  color: inherit;
  font: inherit;
}
.field input:focus,
.field textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.field input[aria-invalid="true"],
.field textarea[aria-invalid="true"] { border-color: #d33; }
/* A textarea as tall as what is in it, from a few lines to where it scrolls,
   rather than a corner to drag: the lines are the thing being counted, and
   all of them should be in view. Where field-sizing is not known the rows
   the server gives it decide. */
.field textarea {
  display: block;
  resize: none;
  line-height: 1.5;
  field-sizing: content;
  min-height: calc(8lh + 1rem + 2px);
  max-height: calc(15lh + 1rem + 2px);
}
.field .error { margin: 0.25rem 0 0; font-size: 0.875rem; }

.actions { display: flex; align-items: center; gap: 1rem; }
.saved { color: var(--muted); font-size: 0.875rem; }

button.secondary {
  background: transparent;
  color: var(--fg);
  border-color: var(--border);
}

/* A button that has been pressed and is waiting. app.js puts the class on, half
   a second after the press rather than at once: an answer that comes back in a
   moment would otherwise flash a spinner and take it away again.

   The icon steps aside for the spinner rather than sitting beside it: a button
   that grew while it worked would move the buttons next to it. One without an
   icon gets the spinner in front of its word, which is the same thing. */
button.is-busy, .button.is-busy {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  /* Not `disabled`, which would grey the button out and, on a plain submit,
     drop the name and value it carries. This only stops a second press. */
  pointer-events: none;
}
button.is-busy > svg, .button.is-busy > svg { display: none; }
button.is-busy::before, .button.is-busy::before {
  content: "";
  flex: none;
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}
@keyframes spin {
  to { transform: rotate(1turn); }
}
/* A dialog on its way: its name, and a spinner where what it holds will be. */
.dialog-loading .loading { display: flex; align-items: center; gap: 0.6rem; margin: 0.5rem 0 1rem; color: var(--muted); }
.spinner {
  flex: none;
  width: 1.1rem;
  height: 1.1rem;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}
/* Something that turns for no reason should turn slowly, or not at all. */
@media (prefers-reduced-motion: reduce) {
  button.is-busy::before, .button.is-busy::before, .spinner {
    animation-duration: 2.4s;
  }
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* A network's mark before a name, where a row's own column has to say which
   network it came from. */
.row-mark { display: inline-flex; align-items: center; gap: 0.4rem; min-width: 0; }
.row-mark svg { flex: none; width: 0.9rem; height: 0.9rem; }

.table-section { margin-top: 2.5rem; }
/* A page that takes over the window: no menu, one thing to do, and a cross to
   leave by. */
.takeover {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: var(--bg);
}
.takeover-bar {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.75rem 1.5rem;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}
/* The badges go under a name too long to leave room for them, and a name with
   nowhere to break -- Search_Spanien_[city]_Broad_Nobrand -- breaks anyway
   rather than push the page wider than the window. */
.takeover-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.6rem; min-width: 0; }
.takeover-title h1 { min-width: 0; margin: 0; font-size: 1.25rem; overflow-wrap: anywhere; }
.takeover-title .muted { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Where a negative keyword list came from, over its search. */
.list-source { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem; }
/* The way out sits first and the name beside it; a page with an action of its
   own puts that at the far end. */
.takeover-action { margin-left: auto; }
.takeover-actions { display: flex; align-items: center; gap: 0.5rem; }
.takeover-actions button svg { width: 1rem; height: 1rem; }
.takeover-actions button { display: inline-flex; align-items: center; gap: 0.35rem; }
.takeover-body { padding: 1.5rem; }
/* A save draws a campaign's settings again in place. Scroll anchoring would
   keep the window on the element that had focus -- the Save button, gone with
   its row shut -- and move it far down; without it the window stays put. */
#campaign-settings { overflow-anchor: none; }
/* A row the page is loaded at -- back from the picker -- clear of the bar that
   stays at the top. */
#campaign-settings details.setting { scroll-margin-top: 5rem; }
/* Where a feed's products come from, under its name. */
.feed-meta { margin: 0 0 1.5rem; }
.takeover-body > .table-section:first-child,
.feed-meta + .table-section { margin-top: 0; }

.section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 0.5rem;
  /* On a narrow screen the buttons drop under the heading rather than pressing
     it into one word per line. */
  flex-wrap: wrap;
}
.section-header h2 { margin: 0; }
/* A section's buttons keep together at the right, however many there are, and
   wrap as a group before any of them breaks its own label in half. */
.section-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; margin-left: auto; }
/* A button says what it does on one line: "Upload file" over two lines is two
   words to read rather than one thing to press. */
.section-actions button, .section-actions .button,
.page-actions button, .page-actions .button { white-space: nowrap; }
.buttons { display: flex; gap: 0.5rem; }
/* A button in the page's heading may carry a mark beside its word, which sits
   on the word's line and at the word's size. */
.page-actions .button { display: inline-flex; align-items: center; gap: 0.4rem; }
.page-actions .button svg { width: 1rem; height: 1rem; }

table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
th, td { padding: 0.5rem 0.75rem 0.5rem 0; text-align: left; border-bottom: 1px solid var(--border); }
th { font-weight: 600; color: var(--muted); }
td.actions-cell { text-align: right; padding-right: 0; }
td.actions-cell form { display: inline-block; }

.badge {
  margin-left: 0.5rem;
  padding: 0.1rem 0.4rem;
  border-radius: 999px;
  background: var(--border);
  font-size: 0.75rem;
  white-space: nowrap;
}

@media (max-width: 40rem) {
  .sessions th:nth-child(3), .sessions td:nth-child(3) { display: none; }
}

.small { font-size: 0.8rem; }

.button.secondary {
  background: transparent;
  color: var(--fg);
  border-color: var(--border);
}

.connect { margin-bottom: 1.5rem; }


table.accounts td { vertical-align: top; }
.status-warning { color: #b7791f; }
.status-error { color: #d33; font-weight: 600; }

.choose-accounts { max-width: 36rem; }
.choose-accounts h2 { margin-top: 0; }
.choices { list-style: none; margin: 0 0 1.5rem; padding: 0; }
.choices li {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--border);
}
.choices input { margin-top: 0.3rem; }
.choices label { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 0.5rem; cursor: pointer; }
.choices label > span:first-child { font-weight: 600; }

.dialog {
  width: min(26rem, calc(100vw - 2rem));
  /* A form with many fields scrolls inside the dialog rather than off-screen. */
  max-height: calc(100vh - 4rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 1.25rem;
  border: 1px solid var(--border);
  border-radius: calc(var(--radius) * 2);
  background: var(--bg);
  color: var(--fg);
  box-shadow: 0 12px 32px rgb(0 0 0 / 0.2);
}
/* A dialog for something that is looked at rather than filled in. */
.dialog-wide { width: min(64rem, calc(100vw - 2rem)); }
.dialog-filters { width: min(40rem, calc(100vw - 2rem)); }
.dialog::backdrop { background: rgb(0 0 0 / 0.4); }
.dialog-header {
  position: sticky;
  top: -1.25rem;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: -1.25rem -1.25rem 1rem;
  padding: 1.25rem 1.25rem 0.75rem;
  background: var(--bg);
}
.dialog-header h2 { margin: 0; }

.platforms { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: 0.5rem; }
.platforms a {
  display: block;
  padding: 0.75rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: inherit;
  font-weight: 600;
  text-decoration: none;
}
.platforms a:hover { border-color: var(--accent); background: var(--border); }

/* Toasts: bottom center, newest last. app.js removes them after a while. */
.toasts {
  position: fixed;
  z-index: 100;
  left: 50%;
  bottom: 1.5rem;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  width: max-content;
  max-width: calc(100vw - 2rem);
  pointer-events: none;
}
.toast {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.6rem 0.75rem 0.6rem 1rem;
  border-radius: var(--radius);
  background: var(--fg);
  color: var(--bg);
  box-shadow: 0 6px 20px rgb(0 0 0 / 0.25);
  pointer-events: auto;
  animation: toast-in 0.2s ease-out;
}
.toast-error { background: #c62828; color: white; }
.toast .icon-button { color: inherit; opacity: 0.7; }
.toast .icon-button:hover { opacity: 1; background: rgb(255 255 255 / 0.15); color: inherit; }
.toast.leaving { opacity: 0; transform: translateY(0.5rem); transition: opacity 0.2s, transform 0.2s; }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(0.5rem); }
}

@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
  .toast.leaving { transition: none; }
}

/* Tables scroll sideways rather than wrap. position: relative makes the scroller
   clip absolutely positioned content too, such as .visually-hidden headers,
   which would otherwise widen the page. */
/* A table sits in a frame with a grey header, like the lists on the other pages. */
.table-scroll {
  position: relative;
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.table-scroll th, .table-scroll td { white-space: nowrap; padding: 0.5rem 0.75rem; }
.table-scroll thead th { background: var(--subtle); }
.table-scroll tbody tr:last-child td { border-bottom: 0; }
.table-scroll td.actions-cell { padding-right: 0.75rem; }
/* With .sticky-first the first column stays in place while the rest scrolls. */
.sticky-first th:first-child, .sticky-first td:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--bg);
}
/* The header keeps its grey where the sticky column crosses it. */
.sticky-first thead th:first-child { background: var(--subtle); }
/* Opaque, so content scrolling under the sticky column stays hidden. */
.table-scroll tbody tr:hover td { background: var(--row-hover); }

/* Show an edge on the sticky column once the table is scrolled. */
@supports (animation-timeline: scroll()) {
  .sticky-first { scroll-timeline: --table-scroll x; }
  .sticky-first th:first-child, .sticky-first td:first-child {
    animation: sticky-column-edge linear both;
    animation-timeline: --table-scroll;
    animation-range: 0 1rem;
  }
  @keyframes sticky-column-edge {
    to { box-shadow: inset -1px 0 0 var(--border), 8px 0 8px -8px rgb(0 0 0 / 0.3); }
  }
}

button.danger, .button.danger { background: #c62828; border-color: #c62828; color: white; }

.danger-zone {
  margin-top: 2.5rem;
  padding: 1rem 1.25rem;
  border: 1px solid #e0a0a0;
  border-radius: var(--radius);
}
.danger-zone h2 { margin: 0 0 0.75rem; color: #c62828; }
.danger-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.danger-row p { margin: 0.25rem 0 0; }

/* Transparent, as the buttons and fields are: the colour of what it sits on,
   white on a card and the page's own on the page. The arrow is drawn here
   rather than by the browser, which sets its own well in from the edge and
   will not be moved: the chevron the setting rows open with, near the right
   edge, in a grey that reads on either theme. */
select {
  /* As tall as a button beside it: the same padding, the same line. */
  padding: 0.5rem 2rem 0.5rem 0.5rem;
  line-height: 1.5;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: transparent url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888888' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M19.5 8.25l-7.5 7.5-7.5-7.5'/%3E%3C/svg%3E") no-repeat right 0.6rem center / 0.9rem;
  color: inherit;
  font: inherit;
  appearance: none;
}
select[multiple] { padding-right: 0.5rem; background-image: none; }
.field select { width: 100%; }

/* A page header: the way back, the page's name, and a quiet line about it. */
/* A page title with its main action on the same line, at the far right. */
.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1rem;
}
.page-header h1 { margin: 0; }
.page-actions { display: flex; align-items: center; gap: 0.5rem; }

.meta {
  margin-top: 0.35rem;
  color: var(--muted);
  font-size: 0.875rem;
}
.meta-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem;
  min-height: 1.75rem;
}
.meta-url {
  max-width: 32rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: inherit;
}
.meta-url:hover { color: var(--accent); }
.meta .icon-button { width: 1.5rem; height: 1.5rem; }
.meta .icon-button svg { width: 1rem; height: 1rem; }

.sync-error {
  margin: -1.5rem 0 1.5rem;
  font-size: 0.8rem;
  color: #d33;
  overflow-wrap: anywhere;
}

table.items input, table.columns input {
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: inherit;
  font: inherit;
  min-width: 12rem;
}
/* A menu keeps the arrow every select has; only its height is the table's. */
table.columns select { padding-top: 0.35rem; padding-bottom: 0.35rem; min-width: 12rem; }
table.columns input[aria-invalid="true"] { border-color: #d33; }
table.items input { width: 100%; min-width: 8rem; }
table.items td { vertical-align: middle; }

/* The empty-state row only shows while the table has no products. */
tbody tr.empty-row:not(:only-child) { display: none; }
tbody tr.empty-row td { border-bottom: 0; }
table.columns td { vertical-align: middle; }
/* The tick for a column in the product ID is a box, not a field. */
table.columns input[type="checkbox"] { min-width: 0; padding: 0; margin: 0; width: 1rem; height: 1rem; vertical-align: middle; }
table.columns .key-cell { width: 1%; text-align: center; }
.key-mark svg { width: 1.1rem; height: 1.1rem; vertical-align: middle; }

/* How well the ticked columns tell the products apart, under the columns. */
/* The block lines up with the table; what is in it reads at a narrower width. */
.product-id { margin-top: 1.25rem; }
.product-id > * { max-width: 32rem; }
.product-id p { margin: 0 0 0.25rem; }
.product-id .ok { display: flex; align-items: center; gap: 0.4rem; color: var(--ok-fg); font-weight: 600; }
.product-id .ok svg { width: 1.1rem; height: 1.1rem; }
.product-id .feed-groups { margin: 0.25rem 0 0.5rem; }
table.feeds td { vertical-align: top; }

.products td { max-width: 24rem; overflow: hidden; text-overflow: ellipsis; }

th.number, td.number { text-align: right; font-variant-numeric: tabular-nums; }
/* The activity table: one line an event, what it was there to open. */
/* In the frame the other lists have: under the bar, the header grey. */
.activity-scroll {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-top: 0;
  border-radius: 0 0 var(--radius) var(--radius);
}
/* The one subject the table is filtered to, over it. */
.activity-heading { display: flex; align-items: baseline; gap: 0.5rem; margin: 0 0 0.75rem; font-weight: 600; }
.activity-heading .activity-name { display: inline-flex; align-items: baseline; gap: 0.4rem; }
.activity-heading .activity-icon { display: inline-flex; align-self: center; color: var(--muted); }
.activity-heading .activity-icon svg { width: 1rem; height: 1rem; }
.activity-heading .muted { font-weight: normal; }
.activity-filtered { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1; }
table.activity { width: 100%; border-collapse: collapse; }
table.activity th { text-align: left; font-weight: 600; color: var(--muted); font-size: 0.875rem; padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--border); background: var(--subtle); }
table.activity td { vertical-align: top; padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--border); }
table.activity tbody tr:last-child td { border-bottom: 0; }
table.activity tbody tr:hover td { background: var(--row-hover); }
table.activity .activity-when { white-space: nowrap; color: var(--muted); }
table.activity .activity-subject { min-width: 10rem; }
table.activity .activity-name { display: inline-flex; align-items: baseline; gap: 0.4rem; }
table.activity .activity-icon { display: inline-flex; align-self: center; color: var(--muted); }
table.activity .activity-icon svg { flex: none; width: 0.9rem; height: 0.9rem; }
table.activity .activity-what { width: 55%; }
table.activity summary { cursor: pointer; list-style: none; }
table.activity summary::-webkit-details-marker { display: none; }
table.activity summary::after { content: " ▸"; color: var(--muted); }
table.activity details[open] > summary::after { content: " ▾"; }
.change-details { margin: 0.5rem 0 0; }
/* A value changed, whole: what differs marked, the rest as it is. */
.change-details .value-changes { list-style: none; padding-left: 0; }
.change-details .value { overflow-wrap: anywhere; }
.change-details del { background: var(--bad-bg); color: var(--bad-fg); text-decoration: none; border-radius: 2px; }
.change-details ins { background: var(--ok-bg); color: var(--ok-fg); text-decoration: none; border-radius: 2px; }
.change-details p { margin: 0 0 0.35rem; }
.change-details ul { margin: 0 0 0.35rem; padding-left: 1.25rem; }
table.activity .activity-type, table.activity .activity-user { white-space: nowrap; }
/* On a phone each event is a block of its own: when and what it happened to
   on a line, then what, then which user. */
@media (max-width: 40rem) {
  table.activity thead { display: none; }
  table.activity tr { display: block; padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--border); }
  table.activity tbody tr:last-child { border-bottom: 0; }
  table.activity tbody tr:hover td { background: none; }
  table.activity td { display: inline; padding: 0; border: 0; }
  table.activity td + td::before { content: " · "; color: var(--muted); }
  table.activity td:empty { display: none; }
  table.activity .activity-what { display: block; width: auto; margin-top: 0.25rem; }
  table.activity .activity-what::before, table.activity .activity-user::before { content: none; }
  table.activity .activity-user { display: block; color: var(--muted); font-size: 0.875rem; }
}
.change-details ul.error { color: var(--bad-fg); }
/* What a send sent: one call a line, what it said and what came back there
   to open. */
.sent-calls ol { margin: 1rem 0; padding-left: 2rem; }
.sent-call { padding: 0.5rem 0; border-bottom: 1px solid var(--border); }
.sent-call-head { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: baseline; }
.sent-call-head code { font-size: 0.8125rem; overflow-wrap: anywhere; }
.sent-call summary { cursor: pointer; color: var(--muted); font-size: 0.875rem; margin-top: 0.25rem; }
.sent-call pre { margin: 0.25rem 0 0; padding: 0.5rem; max-height: 30rem; overflow: auto; background: var(--subtle);
  border-radius: var(--radius); font-size: 0.75rem; white-space: pre-wrap; overflow-wrap: anywhere; }
/* What the feed's sync measured of a picture, under it. */
.picture-meta { display: block; margin-top: 0.25rem; white-space: nowrap; }
.thumb {
  display: block;
  width: 2.5rem;
  height: 2.5rem;
  object-fit: cover;
  border-radius: 4px;
  background: var(--border);
}

/* Radio choices, and fields that depend on the chosen one. */
fieldset.choice { border: 0; margin: 0 0 1rem; padding: 0; }
/* Three short answers to one question sit on a line, where three lines of them
   would read as three questions. */
fieldset.choice.inline { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 1.25rem; }
fieldset.choice.inline legend { flex-basis: 100%; }
fieldset.choice legend { padding: 0; margin-bottom: 0.25rem; font-size: 0.875rem; font-weight: 600; }
label.radio {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.15rem 0;
  font-weight: normal;
  cursor: pointer;
}
/* Radio buttons keep their own size, unlike the form's text fields. */
label.radio input { width: auto; flex: none; margin: 0; }

/* Only ask for the URL when the products come from one, and only offer a
   template when they are added in relaxad. */
.url-field, .manual-field { display: none; }
form:has(input[name="source"][value="url"]:checked) .url-field { display: block; }
form:has(input[name="source"][value="manual"]:checked) .manual-field { display: block; }

/* Example products come with a template, so the choice only applies to one. */
form:has(select[name="template"] option[value="none"]:checked) .examples-field { display: none; }

label.checkbox { display: flex; align-items: center; gap: 0.5rem; font-weight: normal; cursor: pointer; }
label.checkbox input { width: auto; flex: none; margin: 0; }

.icon-button[disabled] { opacity: 0.35; cursor: default; }
.icon-button[disabled]:hover { background: transparent; color: var(--muted); }

/* Dragging rows into another order. */
td.handle-cell { width: 2rem; padding-right: 0; }
.drag-handle {
  display: inline-grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  color: var(--muted);
  cursor: grab;
}
.drag-handle:active { cursor: grabbing; }
.drag-handle svg { width: 1.1rem; height: 1.1rem; }
tr.dragging { opacity: 0.4; }


/* The search above a list: an icon inside the field, like a browser's own. */
.search {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
}
.search-field {
  position: relative;
  display: flex;
  align-items: center;
  flex: 1;
}
.search-icon {
  position: absolute;
  left: 0.6rem;
  display: flex;
  color: var(--muted);
  pointer-events: none;
}
.search-icon svg { width: 1rem; height: 1rem; }
.search-field input {
  width: 100%;
  height: 2.25rem;
  padding: 0 0.6rem 0 2.1rem;
  margin: 0;
  /* The same frame as the list below it, rather than the browser's own. */
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: inherit;
  font: inherit;
  appearance: none;
}
.search-field input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
/* The field's own clear cross, a little larger than the browser draws it. */
.search-field input::-webkit-search-cancel-button {
  -webkit-appearance: none;
  width: 0.9rem;
  height: 0.9rem;
  cursor: pointer;
  background: var(--muted);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='M6 18L18 6M6 6l12 12'/%3E%3C/svg%3E") center / contain no-repeat;
}
.search-field input:hover::-webkit-search-cancel-button { background: var(--fg); }

/* Walking through the pages of a list, under it and in the middle. */
.pager {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.25rem;
  margin: 1rem 0;
  font-size: 0.875rem;
}
.pager a, .pager .step, .pager .gap {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2rem;
  height: 2rem;
  padding: 0 0.5rem;
  border-radius: var(--radius);
  color: var(--fg);
  text-decoration: none;
}
.pager a:hover { background: var(--row-hover); }
.pager .current { background: var(--accent); color: var(--on-accent); font-weight: 600; }
.pager .current:hover { background: var(--accent); }
.pager .gap, .pager .step.disabled { color: var(--muted); }
.pager .step.disabled { cursor: default; }

/* A list of rows with a bar above them. Selecting rows turns the bar into what
   can be done with them; :has() does that, so it needs no JavaScript. */
/* The frame is drawn by the bar and the rows themselves rather than by the form
   around them: the pages sit inside the form too, so that swapping the list
   replaces them, and a frame around the form would enclose those as well. */
.rows { display: block; }
/* The count reads as one thing, so it does not break across two lines, and it
   stands as tall as the buttons it shares the bar with. Everything in the bar
   being one height is what keeps the bar one height: it holds a count until a
   row is ticked and buttons after that, and a bar that grew by the difference
   would nudge the whole page down as soon as anything was picked. */
.rows-count {
  display: inline-flex;
  align-items: center;
  height: 2rem;
  white-space: nowrap;
}

.rows-bar {
  display: flex;
  align-items: center;
  /* The menus go onto a line of their own rather than off the edge when the
     window is too narrow to hold them beside the count. */
  flex-wrap: wrap;
  gap: 0.75rem;
  /* A floor, for a bar holding nothing at all. What keeps it from growing is
     that everything in it is 2rem tall, not this. */
  min-height: 3rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
  background: var(--subtle);
  color: var(--muted);
  font-size: 0.875rem;
}
.rows-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-left: auto;
}

/* The select-all control is a button, since the server renders its state. */
.select-all {
  /* A square, and one that stays square: as a flex item it would otherwise be
     squeezed to a sliver when the bar runs out of room. */
  flex: none;
  width: 1rem;
  height: 1rem;
  padding: 0;
  border: 1px solid var(--muted);
  border-radius: 3px;
  background: var(--bg);
  cursor: pointer;
}
.select-all[data-state="all"], .select-all[data-state="some"] {
  border-color: var(--accent);
  background: var(--accent);
}
.select-all[data-state="all"]::after {
  content: "";
  display: block;
  width: 0.3rem;
  height: 0.55rem;
  margin: -0.1rem auto 0;
  border: solid var(--on-accent);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.select-all[data-state="some"]::after {
  content: "";
  display: block;
  width: 0.5rem;
  height: 2px;
  margin: 0 auto;
  background: var(--on-accent);
}
/* A selected row stands out, as in a mail client. */
.rows-list li:has(input[type="checkbox"]:checked) { background: var(--nav-active); }

/* Everything in the bar is the same height, so the bar keeps its own. */
.rows-actions > button, .rows-actions > a.button, .rows-filters > button, .menu-dropdown > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  height: 2rem;
  padding: 0 0.6rem;
  font-size: 0.875rem;
}
.rows-actions > button svg, .rows-actions > a.button svg, .rows-filters > button svg { width: 1rem; height: 1rem; }

/* A menu opened from a bar: the summary is the button, the panel hangs below it. */
.menu-dropdown { position: relative; margin-left: auto; }
.menu-dropdown > summary {
  list-style: none;
  border-radius: var(--radius);
  color: var(--muted);
  cursor: pointer;
}
.menu-dropdown > summary::-webkit-details-marker { display: none; }
/* A filter of many choices opens a dialog rather than a menu, and looks the
   same as one in the bar. */
.rows-filters > .filter-picker {
  margin-left: auto;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: var(--muted);
  font-weight: normal;
  cursor: pointer;
}
.rows-filters > .filter-picker::after { content: "▾"; }
.rows-filters > .filter-picker:hover { background: var(--row-hover); color: var(--fg); }
.menu-dropdown > summary::after { content: "▾"; }
.menu-dropdown > summary:hover { background: var(--row-hover); color: var(--fg); }
.menu-panel {
  position: absolute;
  z-index: 10;
  right: 0;
  min-width: 13rem;
  /* A team's labels have no set number, so the menu scrolls rather than growing
     out of the window. Finding one among many is the search box's job: typing
     label: there offers them as they are narrowed down. */
  max-height: min(60vh, 22rem);
  overflow-y: auto;
  margin-top: 0.25rem;
  padding: 0.25rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
  text-align: left;
}
/* What a menu says when it has nothing to offer yet. */
.menu-empty { padding: 0.4rem 0.75rem; color: var(--muted); }
.menu-heading {
  padding: 0.35rem 0.5rem;
  font-weight: 600;
  color: var(--fg);
  border-bottom: 1px solid var(--border);
}
.menu-panel ul { list-style: none; margin: 0; padding: 0.25rem 0; }
.menu-panel a {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.5rem;
  border-radius: var(--radius);
  color: var(--fg);
  text-decoration: none;
}
.menu-panel a:hover { background: var(--row-hover); }
.menu-panel a.chosen { font-weight: 600; }
.menu-panel .check { display: flex; width: 1rem; }
.menu-panel .check svg { width: 1rem; height: 1rem; }

/* The rows carry on from the bar above them, whose bottom edge is their top one. */
.rows-list, .rows > .table-scroll {
  border: 1px solid var(--border);
  border-top: 0;
  border-radius: 0 0 var(--radius) var(--radius);
}
.rows-list { list-style: none; margin: 0; padding: 0; }
.rows-list li {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.75rem;
  border-bottom: 1px solid var(--border);
}
.rows-list li:last-child { border-bottom: 0; border-radius: 0 0 calc(var(--radius) - 1px) calc(var(--radius) - 1px); }
.rows-list li:hover { background: var(--row-hover); }
/* What a filter that matched nothing leaves behind, inside the list rather than
   in place of it: the filter is in the header above, and taking the header away
   would leave no way back to it. */
.rows-empty { margin: 0; padding: 0.75rem; }
/* A badge in a row's quiet line is spaced by the line's own gap, not by its
   own margin, which would leave it standing further off than its neighbours. */
.row-meta .badge { margin-left: 0; }
.row-body { min-width: 0; }
.row-name { font-weight: 600; color: var(--fg); text-decoration: none; }
/* Only a name that leads somewhere answers the pointer. */
a.row-name:hover { color: var(--accent); text-decoration: underline; }
.row-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  margin-top: 0.15rem;
  color: var(--muted);
  font-size: 0.8125rem;
}
.select-cell {
  display: flex;
  align-items: center;
  min-height: 1.5rem;
  cursor: pointer;
}
.select-cell input { width: auto; margin: 0; }
.meta-separator { opacity: 0.6; }

/* The campaigns table: what each campaign is, then what it delivered. The name
   column holds the tick box too, so the column that stays in place while the
   numbers scroll is also the one that selects. */
.campaigns td { vertical-align: middle; }
.campaigns .name-cell > div { display: flex; align-items: center; gap: 0.6rem; }
.campaigns .name-cell .row-name { font-weight: 600; }
/* A campaign with many labels makes a tall row; the labels wrap within a width
   rather than stretching the column across the window. */
.campaigns .labels-cell { max-width: 18rem; white-space: normal; }
/* Every heading orders the table by its column. */
.campaigns th a {
  display: inline-flex;
  align-items: baseline;
  gap: 0.3rem;
  color: inherit;
  text-decoration: none;
}
.campaigns th a:hover { color: var(--accent); }
.campaigns th[aria-sort] a { color: var(--accent); }
.campaigns .sort-arrow { font-size: 0.75rem; }
/* A number nobody has recorded yet is a dash, and stays out of the way. */
.campaigns td.number.muted { color: var(--muted); }


/* The menus that narrow a list sit together at the right of its bar. Each one
   pushes itself right on its own, so grouped they share the free space between
   them; inside the group they follow one another instead. */
.rows-filters {
  display: flex;
  align-items: center;
  /* They wrap among themselves before the bar breaks, and each row of them
     stays at the end where they belong. */
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.25rem;
  margin-left: auto;
}
.rows-filters .menu-dropdown { margin-left: 0; }
/* A menu per filter while they fit; below, one menu holding all of them. Five
   menus is more chrome than a phone can spare above a table. The days the
   numbers cover stay beside it either way: they change what the numbers mean
   rather than which campaigns are listed.

   The narrow rule sits here rather than with the rest of the stacked layout,
   since it and the rule it overrides carry the same weight and the later one
   would otherwise win whatever the window is doing. */
.filters-wide { display: flex; align-items: center; gap: 0.25rem; }
.filters-narrow { display: none; }

@media (max-width: 40rem) {
  .filters-wide { display: none; }
  .filters-narrow { display: block; }
}

/* The state a campaign is in, as a badge: green while it runs, grey while it is
   paused, red once it has been removed. */
.status-badge {
  display: inline-block;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap;
}
/* What something runs on: outlined, so a column of them does not read as a
   column of statuses. */
.type-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.1rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--subtle);
  color: var(--muted);
  font-size: 0.75rem;
  white-space: nowrap;
}
/* A brand mark keeps its own size whatever it stands next to. */
.type-badge svg { flex: none; width: 0.75rem; height: 0.75rem; }
.platform-name { display: inline-flex; align-items: center; gap: 0.4rem; }
.platform-name svg { flex: none; width: 1rem; height: 1rem; }

.status-running { background: var(--ok-bg); color: var(--ok-fg); }
.status-paused { background: var(--border); color: var(--fg); }
.status-removed { background: var(--bad-bg); color: var(--bad-fg); }

/* The top of a campaign's page: what it is beside what its ads look like, as
   two cards side by side, one over the other on a phone. */
.campaign-overview {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 1rem;
}
.overview-facts,
.overview-preview {
  margin: 0;
  padding: 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.overview-facts { display: grid; align-content: start; gap: 1rem; }
.overview-facts dt { color: var(--muted); font-size: 0.875rem; }
.overview-facts dd { display: flex; flex-direction: column; margin: 0.15rem 0 0; }
.overview-facts .overview-sync dd, .overview-facts .overview-modified dd {
  flex-direction: row; justify-content: space-between; align-items: baseline;
}
.overview-facts button.link, .overview-facts a { text-decoration: none; }
.overview-sync-actions { display: flex; gap: 1rem; }
/* The sync plan: what the network would refuse first, then the campaign and
   its ad groups, each ad a row of what it says. */
.sync-plan section { margin-top: 1.5rem; }
.sync-plan h2 { margin: 0 0 0.5rem; font-size: 1rem; }
.plan-problems { padding: 0.75rem 1rem; border-radius: var(--radius); background: var(--bad-bg); color: var(--bad-fg); }
.plan-problems ul { margin: 0; padding-left: 1.25rem; }
.plan-check .plan-problems { margin-top: 0.75rem; }
.plan-passed { padding: 0.75rem 1rem; border-radius: var(--radius); background: var(--ok-bg); color: var(--ok-fg); font-weight: 600; }
.plan-ads { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.plan-ads th, .plan-ads td { padding: 0.35rem 0.5rem; border-top: 1px solid var(--border); text-align: left; vertical-align: top; }
.plan-ads th { color: var(--muted); font-weight: 600; }
.plan-url { overflow-wrap: anywhere; }
.overview-facts button.link:disabled { color: var(--muted); cursor: default; }
.overview-preview { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem; }
/* Where the preview is drawn: its room held whether or not there is an ad. */
.preview-stage {
  display: flex;
  flex: 1;
  align-items: center;
  justify-content: center;
  min-height: 14rem;
  padding: 1.5rem;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--fg) 5%, var(--surface));
}
/* A search ad as Google draws one, in Google's colours whatever this page's
   theme: it is a picture of a results page, not a part of this one. */
.search-ad {
  width: 100%;
  max-width: 40rem;
  padding: 1rem 1.25rem;
  border: 1px solid #dadce0;
  border-radius: 8px;
  background: #ffffff;
  color: #202124;
  font-family: Arial, sans-serif;
}
.search-ad p { margin: 0; }
.search-ad .search-ad-sponsored { font-size: 0.875rem; font-weight: 700; }
.search-ad .search-ad-url { margin-top: 0.35rem; font-size: 0.875rem; overflow-wrap: anywhere; }
.search-ad .search-ad-headline { margin-top: 0.35rem; color: #1a0dab; font-size: 1.25rem; line-height: 1.3; }
.search-ad .search-ad-description { margin-top: 0.4rem; color: #4d5156; font-size: 0.875rem; line-height: 1.45; }
.search-ad .search-ad-callouts { margin-top: 0.3rem; color: #4d5156; font-size: 0.875rem; }
/* The text, and the picture beside it where there is one, as Google puts an
   image asset: a small square to the right. */
.search-ad-body { display: flex; align-items: flex-start; gap: 1rem; }
.search-ad-text { flex: 1; min-width: 0; }
.search-ad .search-ad-image {
  flex: none;
  width: 5.5rem;
  height: 5.5rem;
  margin-top: 0.35rem;
  border-radius: 8px;
  object-fit: cover;
}
.preview-empty { margin: 0; text-align: center; }
/* A responsive display ad as a card, in Google's colours whatever this page's
   theme: the picture over the text, the button to the site at its corner. */
/* A Facebook ad as the feed shows it: who it is from, the text, the picture
   square, and the grey bar under it with the site, the headline and the
   button -- in Facebook's own colours, as the Google ads are in Google's. */
/* A campaign here beside as Facebook has it, setting by setting, those that
   differ marked. */
.facebook-compare { width: 100%; border-collapse: collapse; margin-top: 1rem; background: var(--surface); }
.facebook-compare th, .facebook-compare td { padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--border); text-align: left; vertical-align: top; overflow-wrap: anywhere; }
.facebook-compare thead th { color: var(--muted); font-size: 0.8rem; font-weight: 600; }
.facebook-compare tbody th { width: 12rem; font-weight: 600; }
.facebook-compare tr.differs td:last-child { font-weight: 600; }
.compare-pictures { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.facebook-compare tr.differs th::after { content: " •"; color: var(--accent); }

/* A choice of two ways, each said under its name; the positions to tick only
   where placing by hand is chosen. */
.placement-modes { display: grid; gap: 0.6rem; margin-bottom: 1rem; }
.placement-mode { display: flex; align-items: flex-start; gap: 0.5rem; cursor: pointer; }
.placement-mode input { margin-top: 0.2rem; }
.placement-mode > span { display: grid; gap: 0.1rem; }
.placements-manual { display: none; gap: 1rem; margin-bottom: 1rem; }
.placements-choice:has(input[name="placement"][value="manual"]:checked) .placements-manual { display: grid; }
.placement-platform { margin: 0; padding: 0; border: 0; }
.placement-platform legend { margin-bottom: 0.4rem; padding: 0; font-weight: 600; }

/* A catalog ad's products, a card each, swiped through side by side. */
.fb-carousel { display: flex; gap: 0.5rem; overflow-x: auto; padding: 0 0.75rem 0.75rem; scroll-snap-type: x mandatory; }
.fb-card { flex: none; width: 70%; overflow: hidden; border: 1px solid #dadde1; border-radius: 8px; background: #f0f2f5; scroll-snap-align: start; }
.fb-card-image { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; background: #fff; }
.fb-card-text { padding: 0.5rem 0.6rem 0.25rem; }
.fb-card-cta { display: block; margin: 0.25rem 0.6rem 0.6rem; text-align: center; }

/* From and to side by side. */
.age-range { display: flex; gap: 1rem; }

.fb-ad {
  width: 100%;
  max-width: 22rem;
  overflow: hidden;
  border: 1px solid #dadde1;
  border-radius: 8px;
  background: #fff;
  color: #050505;
  font-family: system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  text-align: left;
}
.fb-ad p { margin: 0; }
.fb-ad-head { display: flex; align-items: center; gap: 0.5rem; padding: 0.75rem 0.75rem 0.5rem; }
.fb-ad-avatar {
  display: flex; flex: none; align-items: center; justify-content: center;
  width: 2.5rem; height: 2.5rem; border-radius: 50%;
  background: #e4e6eb; color: #65676b; font-weight: 700;
}
img.fb-ad-avatar { object-fit: cover; }
.fb-ad .fb-ad-name { font-size: 0.9rem; font-weight: 600; line-height: 1.2; }
.fb-ad .fb-ad-sponsored { color: #65676b; font-size: 0.8rem; }
/* Cut short after three lines, as Facebook does with "... See more". */
.fb-ad .fb-ad-primary {
  display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
  margin: 0 0.75rem 0.6rem; font-size: 0.9rem; line-height: 1.35; overflow-wrap: anywhere;
}
.fb-ad-image { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; background: #f0f2f5; }
.fb-ad-no-image { display: flex; align-items: center; justify-content: center; color: #65676b; font-size: 0.8rem; }
.fb-ad-link { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 0.75rem; background: #f0f2f5; }
.fb-ad-link-text { flex: 1; min-width: 0; }
.fb-ad .fb-ad-site { overflow: hidden; color: #65676b; font-size: 0.75rem; text-overflow: ellipsis; white-space: nowrap; }
.fb-ad .fb-ad-headline {
  display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  font-size: 0.95rem; font-weight: 600; line-height: 1.25;
}
.fb-ad .fb-ad-description { overflow: hidden; color: #65676b; font-size: 0.85rem; text-overflow: ellipsis; white-space: nowrap; }
.fb-ad-cta {
  flex: none; padding: 0.45rem 0.8rem; border-radius: 6px;
  background: #e4e6eb; color: #050505; font-size: 0.85rem; font-weight: 600;
}

/* An OpenAI chat card: the picture beside the text rather than over it, and
   each line cut with an ellipsis rather than wrapped -- the card is a fixed
   height in the chat, so a long title pushes nothing down. */
.chat-ad {
  display: flex;
  width: 100%;
  max-width: 22rem;
  overflow: hidden;
  border: 1px solid #dedede;
  border-radius: 12px;
  background: #fff;
  color: #202123;
  font-family: system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  text-align: left;
}
.chat-ad p { margin: 0; }
.chat-ad-image { display: block; flex: none; width: 7.25rem; height: 7.25rem; object-fit: cover; background: #f3f3f3; }
.chat-ad-no-image { display: flex; align-items: center; justify-content: center; color: #6f6f6f; font-size: 0.8rem; }
.chat-ad-text { display: flex; flex: 1; min-width: 0; flex-direction: column; gap: 0.15rem; padding: 1rem 0.75rem 0.75rem 0.95rem; }
.chat-ad-head { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
.chat-ad .chat-ad-name { overflow: hidden; flex: 1; min-width: 0; font-size: 0.8rem; text-overflow: ellipsis; white-space: nowrap; }
.chat-ad-badge { flex: none; padding: 0.1rem 0.5rem; border-radius: 9px; background: #f1f1f1; color: #666; font-size: 0.75rem; }
.chat-ad .chat-ad-title { overflow: hidden; font-size: 0.9rem; line-height: 1.3; text-overflow: ellipsis; white-space: nowrap; }
.chat-ad .chat-ad-body { overflow: hidden; color: #6f6f6f; font-size: 0.9rem; line-height: 1.3; text-overflow: ellipsis; white-space: nowrap; }

.display-ad {
  position: relative;
  width: 100%;
  max-width: 20rem;
  overflow: hidden;
  border: 1px solid #dadce0;
  border-radius: 8px;
  background: #ffffff;
  color: #202124;
  font-family: Arial, sans-serif;
}
.display-ad-image { display: block; width: 100%; aspect-ratio: 1.91; object-fit: cover; background: #f1f3f4; }
.display-ad-no-image { display: flex; align-items: center; justify-content: center; color: #80868b; font-size: 0.8rem; }
.display-ad-body { position: relative; padding: 0.75rem 1rem 1rem; }
.display-ad-brand { display: flex; align-items: center; gap: 0.4rem; min-height: 1.25rem; color: #5f6368; font-size: 0.75rem; }
.display-ad-logo { width: 1.25rem; height: 1.25rem; border-radius: 3px; object-fit: contain; }
.display-ad .display-ad-headline { margin: 0.4rem 2.75rem 0 0; font-size: 1.05rem; font-weight: 700; line-height: 1.3; }
.display-ad .display-ad-description { margin: 0.3rem 2.75rem 0 0; color: #5f6368; font-size: 0.85rem; line-height: 1.4; }
.display-ad-cta {
  position: absolute;
  right: 1rem;
  bottom: 1rem;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  background: #1a73e8;
  color: #ffffff;
  font-size: 1.4rem;
  line-height: 1;
}
/* An asset's row with the ad beside it, on a screen with room for both: the
   fields as wide as they are anyway, the ad in what is left. Narrower, the ad
   at the top of the page is the one to look at. */
.with-preview { display: flex; align-items: flex-start; gap: 2rem; }
.with-preview > .row-edit { flex: 0 1 36rem; min-width: 0; }
.with-preview > .row-preview { flex: 1; min-width: 20rem; max-width: 36rem; }
@media (max-width: 72rem) {
  .with-preview { display: block; }
  .with-preview > .row-preview { display: none; }
}

/* The ad group whose ads the preview shows, under it beside the arrows. */
.preview-adgroups { flex: 1; min-width: 0; }
.preview-adgroups select { width: 100%; }
/* From one ad to the next, under the preview. */
.preview-browser { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0 0.25rem; }
.preview-steps { display: flex; flex: none; align-items: center; gap: 0.5rem; margin-left: auto; }
.preview-steps button { min-width: 2.5rem; padding: 0.5rem 0.6rem; }
.preview-steps button:disabled { opacity: 0.4; cursor: default; }

@media (max-width: 48rem) {
  .campaign-overview { grid-template-columns: minmax(0, 1fr); }
}

/* A campaign's settings, each a line saying what it is until somebody opens it.
   Read far more often than changed, so what they are is what is shown. */
.settings-group { margin-top: 2rem; }
.settings-title { margin: 0 0 0.5rem; font-size: 0.875rem; font-weight: 600; color: var(--muted); }
/* The rows of a box sit together as one, and an open row stands apart from its
   neighbours: what it holds is being worked on, and reads as a thing of its
   own. The border is on the rows rather than the box so the box can part. */
.setting { border: 1px solid var(--border); background: var(--surface); }
.setting:first-child { border-radius: var(--radius) var(--radius) 0 0; }
.setting:last-child { border-bottom-left-radius: var(--radius); border-bottom-right-radius: var(--radius); }
.setting + .setting { border-top: 0; }
.setting[open] + .setting,
.setting + .setting[open] {
  margin-top: 0.75rem;
  border-top: 1px solid var(--border);
  border-top-left-radius: var(--radius);
  border-top-right-radius: var(--radius);
}
.setting:has(+ .setting[open]),
.setting[open]:has(+ .setting) {
  border-bottom-left-radius: var(--radius);
  border-bottom-right-radius: var(--radius);
}
/* The hover follows the row's corners rather than showing square through them. */
.setting > summary { border-radius: inherit; }
.setting[open] > summary { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.setting > summary {
  display: flex;
  align-items: baseline;
  gap: 1.5rem;
  padding: 0.9rem 1rem;
  cursor: pointer;
  list-style: none;
}
.setting > summary::-webkit-details-marker { display: none; }
.setting-label { flex: none; width: 11rem; font-weight: 600; }
/* One line, however much it says: the rest is in the open row. */
.setting-summary { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }
/* Open, the row shows what it is set to in full below, so the line saying it
   in short gives way. Hidden rather than taken out, so nothing on the line
   moves as the row opens and shuts. */
details.setting[open] > summary .setting-summary { visibility: hidden; }
/* A row opens and shuts with a short slide -- short enough not to be waited
   for -- and the chevron turns over with it. A browser that cannot animate to
   a height it does not know opens the row at once, as it always did.
   Clipped only while shut or shutting: open, what hangs below a field -- the
   columns a $ offers -- reaches past the row as it must. Opening, what the row
   holds is under the rows below it until they have moved out of the way. */
.setting { interpolate-size: allow-keywords; transition: margin-top 180ms ease; }
.setting::details-content {
  height: 0;
  overflow: clip;
  transition: height 180ms ease, content-visibility 180ms allow-discrete;
}
.setting[open]::details-content { height: auto; overflow: visible; }
/* Put open without sliding, to slide shut from there: see app.js. */
.setting.at-once, .setting.at-once::details-content { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .setting, .setting::details-content { transition: none; }
}
/* The pictures a shut Images row has chosen, small, in place of words. A
   little taller than the line, and the row kept its height by letting them
   reach into its padding. */
.summary-thumbs { display: inline-flex; gap: 0.3rem; margin-block: -0.35rem; vertical-align: middle; }
/* Nothing to cut short with an ellipsis, and the pictures reach past the line. */
.setting-summary:has(.summary-thumbs) { overflow: visible; }
/* As high as each other, as wide as their shape makes them. */
.summary-thumbs img {
  width: auto;
  max-width: 5rem;
  height: 2rem;
  border-radius: 4px;
  object-fit: cover;
  background: var(--subtle);
}
/* A note at the far end of the line -- "2 ad groups differ" -- apart from what
   the setting is, and a shade darker than it. */
.setting-note {
  flex: none;
  margin-left: auto;
  color: color-mix(in srgb, var(--fg) 75%, var(--muted));
  font-size: 0.875rem;
  white-space: nowrap;
}
.setting-note + .setting-chevron { margin-left: 0; }
/* A note of something the platform will refuse or ignore: "Needs 1:1". */
.setting-note.warning { color: var(--bad-fg); }
.setting-chevron { margin-left: auto; color: var(--muted); }
.setting-chevron svg { display: block; width: 1.1rem; height: 1.1rem; }
.setting[open] .setting-chevron { transform: rotate(180deg); }
.setting-detail { padding: 0 1rem 1rem calc(11rem + 2.5rem); }
.setting-facts { display: grid; grid-template-columns: auto 1fr; gap: 0.3rem 1rem; margin: 0 0 1rem; }
.setting-facts dt { color: var(--muted); }
.setting-facts dd { margin: 0; }
.setting-actions { display: flex; gap: 0.5rem; }
/* The Feed row's Close stands apart from the table and pager above it. */
.setting-actions.feed-close { margin-top: 1.5rem; }

@media (max-width: 40rem) {
  /* Stacked, the label sits over what it says rather than beside it, and the
     chevron keeps to the right of both however long the line under it is. */
  .setting > summary {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 0.25rem 1rem;
  }
  .setting-label { width: auto; }
  .setting-summary { grid-row: 2; }
  .setting-note { grid-row: 3; margin-left: 0; }
  .setting-chevron { grid-column: 2; grid-row: 1 / span 3; margin-left: 0; }
  .setting-detail { padding-left: 1rem; }
}

/* An amount is a short number; a field the width of the form says otherwise. */
.amount { max-width: 10rem; }
.budget-form { max-width: none; }
/* A menu in a setting is as wide as its longest choice needs, not as wide as
   the row: "Target CPA" across the page reads as a field waiting for more. */
.budget-form .field select { width: auto; min-width: 12rem; max-width: 100%; }
/* A target is a number or a sum over the feed's columns -- $price * 0.1 -- so
   room for the second without a box across the page for the first. */
.field input.target { max-width: 20rem; }
/* One adjustment factor, or for view and click conversions one of each: the
   menu says which. */
.conversion-form:has(option[value="purchases_view_and_click"]:checked) .factor-factor,
.conversion-form:not(:has(option[value="purchases_view_and_click"]:checked)) .factor-view-factor,
.conversion-form:not(:has(option[value="purchases_view_and_click"]:checked)) .factor-click-factor { display: none; }
/* No target field under a strategy that takes none: the menu says which do. */
.bidding-form:has(option[data-target="none"]:checked) .bid-target { display: none; }

/* A part of a setting, named small above what it holds, and what it is set
   to with the pencil that changes it. */
.setting-part { margin: 0 0 0.25rem; font-size: 0.875rem; font-weight: 400; color: var(--muted); }
.setting-value + .setting-part { margin-top: 1rem; }
.setting-value { display: flex; align-items: center; gap: 0.25rem; margin: 0; }
/* A campaign's filters in their dialog: a column, a comparison and a value on
   one line, and the cross that takes the line away. Not .field, whose rules
   would stretch each of the three across the dialog. */
.filter-rows { margin-bottom: 1rem; }
.filter-row { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
/* The menus are drawn as every menu is; the value field to match them. */
.filter-row select { flex: 0 1 9rem; min-width: 0; }
.filter-row input {
  flex: 1 1 8rem;
  min-width: 0;
  padding: 0.5rem;
  line-height: 1.5;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: transparent;
  color: inherit;
  font: inherit;
}
.filter-rows > .muted { margin: 0 0 0.75rem; }
/* The products a campaign keeps, under its feed settings: no taller than a
   box, scrolling within it, the column names staying at the top. */
#campaign-products { margin-top: 1.25rem; }
.campaign-products { max-height: 500px; overflow: auto; }
.campaign-products thead th { position: sticky; top: 0; z-index: 2; }
/* Under the table, at its right. */
.campaign-products-pager .pager { justify-content: flex-end; margin: 0.75rem 0 0; }
/* What a choice of ad groups comes to, under it in the dialog: the count, and
   the groups in a list that scrolls rather than stretching the dialog. */
.ad-groups-summary { margin-bottom: 1rem; }
.ad-groups-summary:empty { display: none; }
.ad-groups-summary .setting-value { font-weight: 600; }
.ad-groups-summary .feed-groups { max-height: 16rem; overflow-y: auto; margin-top: 0.5rem; }
/* The feed dialog's list of feeds: a row each, the chosen one marked, the
   list scrolling rather than the dialog growing past the window. */
.feed-choices {
  max-height: 20rem;
  overflow-y: auto;
  margin: 0 0 1rem;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.feed-choice {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.6rem 0.75rem;
  border-bottom: 1px solid var(--border);
  font-weight: normal;
  cursor: pointer;
}
.feed-choice:last-child { border-bottom: 0; }
.feed-choice:hover { background: var(--row-hover); }
.feed-choice:has(input:checked) { background: var(--nav-active); }
.feed-choice input { width: auto; flex: none; margin: 0; }
.feed-choice .muted { margin-left: auto; white-space: nowrap; }
/* One ad group has no column to choose. */
.form:has(.ad-groups-choice input[value="one"]:checked) .group-column { display: none; }

/* A list of values and how many products carry each. */
.feed-groups { list-style: none; margin: 0.75rem 0 0; padding: 0; }
.feed-groups li {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.3rem 0;
  border-bottom: 1px solid var(--border);
}
.feed-groups li:last-child { border-bottom: 0; }

/* A dashboard: sections one under another, each with its heading and its menu
   above what it holds. */
.dash-section { margin-bottom: 2rem; }
/* While the page is arranged, a section is a thing on the page to be moved
   about rather than a heading over some cards, so it is drawn as one. */
.dash-sections[data-sortable] .dash-section {
  cursor: grab;
  margin-bottom: 1rem;
  padding: 0.75rem 1rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--subtle);
}
.dash-section.dragging { opacity: 0.4; }
.dash-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 2rem;
  margin-bottom: 0.75rem;
}
.dash-head h2 { margin: 0; font-size: 1.125rem; }
/* What a section with no heading is called while the page is arranged. It is
   not a name, so it does not read as one. */
.dash-head h2.nameless { color: var(--muted); font-weight: 500; }
/* The menu stands beside the name, not at the far end of the line. */
.dash-menu { margin-right: auto; }
/* A menu of things to do rather than places to go: its summary is an icon, and
   what is in it are buttons that look like the links in the other menus. */
/* An icon on its own, so it is square and wears no caret: the arrow beside a
   word says the word opens something, and an icon that is a pencil has already
   said it. */
.actions-menu > summary { width: 2rem; justify-content: center; padding: 0; }
.actions-menu > summary::after { content: none; }
.actions-menu > summary svg { display: block; width: 1.125rem; height: 1.125rem; }
.menu-action {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.4rem 0.75rem;
  border: 0;
  background: none;
  color: var(--fg);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.menu-action:hover { background: var(--row-hover); }
.menu-action:disabled { opacity: 0.4; cursor: default; }
.menu-action:disabled:hover { background: none; }
/* The bar's menu of changes: a button like the ones beside it, pencil and
   word, which opens rather than does. Its panel hangs from its right edge, as
   the bar ends there. */
.edit-menu { margin-left: 0; }
.edit-menu > summary {
  border: 1px solid var(--border);
  color: var(--fg);
}
.edit-menu > summary svg { display: block; width: 1rem; height: 1rem; }
/* The saved searches, looked after on a page of their own. */
.favourites td { vertical-align: middle; }
.favourites code { font-size: 0.8125rem; }
.menu-action svg { width: 1rem; height: 1rem; }

/* A section's cards, in a row that wraps. Every kind of card is the same shape
   and the same width: what tells them apart is what each of them says. Wide
   enough for the line under the number to hold the days and the networks beside
   each other, since that line is the one that grew. */
.card-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: 1rem;
}
/* While the page is arranged the cards can be dragged, which the pointer says
   before anything is picked up. */
.card-grid[data-sortable] > .dash-card { cursor: grab; }
.dash-sections[data-sortable] .dash-menu { cursor: auto; }
/* Somewhere to drop the first card of a section that has none. It stands where
   a card would, and goes as soon as one is there -- including while one is
   being dragged in. */
.card-empty {
  display: grid;
  place-items: center;
  min-height: 5rem;
  padding: 1rem;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  font-size: 0.875rem;
}
.card-grid:has(.dash-card) .card-empty { display: none; }
.dash-card.dragging { opacity: 0.4; }
.dash-card {
  padding: 1rem 1.1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
}
.card-head { display: flex; align-items: center; gap: 0.5rem; min-height: 1.5rem; }
.card-label { color: var(--muted); font-size: 0.8125rem; }
/* A card standing for a saved search is a way into that list, so its heading is
   the way in rather than a label beside one. */
a.card-label { color: inherit; text-decoration: none; }
a.card-label:hover { color: var(--accent); text-decoration: underline; }
/* A card's own menu, at the right of its heading. It is there whether or not
   the card is pointed at: something that appears under the pointer is something
   nobody finds who is not already looking for it. */
.card-menu { margin-left: auto; }
.card-value {
  /* Lining figures of one width, so a column of numbers lines up. */
  font-variant-numeric: tabular-nums;
  margin: 0.15rem 0 0.35rem;
  font-size: 1.875rem;
  font-weight: 600;
  line-height: 1.2;
}
.card-period { color: var(--muted); font-size: 0.8125rem; }
/* The networks the number covers, on a line of their own at the foot of the
   card: their marks alone, since the name beside a mark is the same name on
   every card that carries one. */
.card-platforms {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.6rem;
}
.card-platform svg { display: block; width: 1.125rem; height: 1.125rem; }

/* Uploaded images and videos, as tiles: what a file is, is easier seen than
   read. The tiles grow to fill the row and wrap when they run out of it. */
/* The bar stands on its own above the tiles rather than at the head of a list
   with an edge, so it is a box in its own right. */
#media-list .rows-bar { border-radius: var(--radius); }
.media-grid {
  list-style: none;
  margin: 0;
  padding: 1rem 0 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: 1rem;
}
.media-tile { min-width: 0; }
/* A rectangle of its own, so a row of tiles lines up whatever shape the files
   are, and the picture fills it. Cropped, which loses the edges of anything that
   is not this shape -- a row of pictures at one size reads better than a row of
   pictures at their own, and what a file is whole is one click away. The grey
   behind it is what shows while a picture is still loading. */
.media-thumb {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--subtle);
}
.media-thumb img, .media-thumb video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* The tick box sits on the picture with nothing behind it, and is told apart
   from whatever it lands on by a shadow rather than by a patch of white. */
.media-pick {
  position: absolute;
  top: 0.4rem;
  left: 0.4rem;
}
.media-pick input {
  filter: drop-shadow(0 0 1px rgb(0 0 0 / 0.9)) drop-shadow(0 1px 2px rgb(0 0 0 / 0.5));
}
/* The image picker: the whole picture is the way to tick it, a ticked one
   outlined; one the campaign has already is there, faded, and says so. */
label.media-thumb { display: block; cursor: pointer; }
.media-thumb:has([data-pick]:checked) { outline: 3px solid var(--accent); outline-offset: -3px; }
.media-tile.added .media-thumb { cursor: default; }
.media-tile.added img { opacity: 0.45; }
.media-added {
  position: absolute;
  top: 0.4rem;
  left: 0.4rem;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  background: var(--surface);
  color: var(--fg);
  font-size: 0.75rem;
  font-weight: 600;
}
.picker-actions { display: flex; align-items: center; gap: 0.5rem; }
.picker-actions form { margin: 0; }
.picker-upload input[type="file"] { display: none; }
.media-about { margin-top: 0.4rem; }
/* A file name is one line, cut where it runs out of tile; the whole of it is in
   the title and a click away. */
.media-about .row-name {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The picture is the button that opens the preview: no chrome of its own, since
   what is on it is the whole of it. */
.media-open {
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: zoom-in;
}
.media-open:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* A video says it is one, since it does not play until the preview is open. */
.media-play {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.media-play svg {
  width: 2.5rem;
  height: 2.5rem;
  color: #fff;
  filter: drop-shadow(0 1px 3px rgb(0 0 0 / 0.6));
}

/* A file at the size the window allows, with what is known about it underneath. */
.media-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 0.75rem;
  background: var(--subtle);
  border-radius: var(--radius);
}
.media-preview img, .media-preview video {
  display: block;
  max-width: 100%;
  max-height: calc(100vh - 16rem);
  border-radius: var(--radius);
}

/* A campaign's labels, beside its name and in the dialog that sets them. */
.labels { display: inline-flex; flex-wrap: wrap; gap: 0.25rem; }
.label-chip {
  padding: 0.05rem 0.45rem;
  border-radius: 999px;
  background: var(--nav-active);
  color: var(--accent);
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap;
}
.label-choices { list-style: none; margin: 0 0 1rem; padding: 0; }
.label-choices li { padding: 0.25rem 0; }

/* What would finish the word being typed in the search box, hanging under it.
   The menu is taken out of the row's flow: as a box of its own beside the field
   it would be a flex item of no width, with nothing to show. */
.search-box { position: relative; display: flex; flex: 1; }
.suggestions {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 10;
}
.suggestions .menu-panel {
  position: static;
  right: auto;
  min-width: 0;
  max-height: 16rem;
  overflow-y: auto;
  margin-top: 0.15rem;
  list-style: none;
  padding: 0.25rem;
}
.suggestions a {
  display: block;
  padding: 0.35rem 0.5rem;
  border-radius: var(--radius);
  color: var(--fg);
  text-decoration: none;
}
.suggestions a:hover { background: var(--row-hover); }
/* Where the arrow keys are in that menu, which hovering does not move. */
.suggestions a.active { background: var(--nav-active); color: var(--accent); }

/* Each headline as the first product makes it, with its length beside it. */
/* Texts one a line, each as wide as its longest may be and no wider -- thirty
   characters in a box across the page reads as a mistake -- and each as tall
   as there may be of them. */
.lines-field-headlines,
.lines-field-callouts,
.lines-field-business-name { max-width: 28rem; }
.lines-field-long-headline { max-width: 44rem; }
.lines-field-descriptions,
.lines-field-keywords,
.lines-field-negative-keywords { max-width: 44rem; }
.lines-field-descriptions textarea { min-height: calc(4lh + 1rem + 2px); }

/* The feed's columns under a field that can take from the feed, offered as a $
   is typed: the same menu as the search box's, with what each column holds
   for the first product beside its name. */
.ref-box { position: relative; }
.ref-box .suggestions a { display: flex; gap: 0.75rem; align-items: baseline; cursor: pointer; }
.ref-name { flex: none; }
.ref-example { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.85em; }

/* Tick boxes in a setting: a place or a language a line, the languages in
   columns since there are dozens, genders and ages side by side since there are
   a handful. What a search found sits apart from what is chosen. */
.ticks { list-style: none; margin: 0.5rem 0 1rem; padding: 0; }
.ticks label { display: flex; align-items: baseline; gap: 0.5rem; padding: 0.2rem 0; cursor: pointer; }
.ticks.found { padding-top: 0.5rem; border-top: 1px dashed var(--border); }
.ticks.columns { columns: 12rem; column-gap: 1.5rem; }
.ticks.columns li { break-inside: avoid; }
.ticks.inline { display: flex; flex-wrap: wrap; gap: 0 1.5rem; }
/* A setting set for the campaign and, apart from it, per ad group: two tabs
   over the row's form. */
.tabs { display: flex; gap: 1.25rem; margin: 0 0 1rem; border-bottom: 1px solid var(--border); }
/* Buttons of the row's form, since going to the other tab takes along what was
   changed on this one, but looking like tabs rather than buttons. */
button.tab {
  margin-bottom: -1px;
  padding: 0.35rem 0;
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  background: none;
  color: var(--muted);
}
button.tab:hover { color: var(--fg); }
button.tab[aria-selected="true"] { border-bottom-color: var(--accent); color: var(--fg); font-weight: 600; }
/* From one ad group to the next, beside what is set for it. */
.adgroup-browser { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-bottom: 1rem; }
.adgroup-browser select { width: auto; max-width: 16rem; }
.adgroup-browser button { min-width: 2.5rem; }
.adgroup-browser button:disabled { opacity: 0.4; cursor: default; }
/* The ad group's own choices show when its box is ticked, and what it takes
   from the campaign when not -- the stylesheet deciding, so no script has to. */
.own-toggle { display: flex; align-items: baseline; gap: 0.5rem; cursor: pointer; }
.own-choices { margin-left: 1.5rem; }
.own-setting .inherited { margin: 0.25rem 0 1rem 1.5rem; }
.own-setting:has(.own-toggle input:not(:checked)) .own-choices,
.own-setting:has(.own-toggle input:checked) .inherited { display: none; }

/* The Images row: two tabs over two panels of one form, the stylesheet showing
   whichever is ticked -- both are sent, whichever is showing. The tabs are
   labels of radio buttons, drawn as the row tabs are. */
label.tab { display: inline-block; margin-bottom: -1px; padding: 0.35rem 0; border-bottom: 2px solid transparent; color: var(--muted); cursor: pointer; }
label.tab:hover { color: var(--fg); }
label.tab input { position: absolute; opacity: 0; pointer-events: none; }
label.tab:has(input:checked) { border-bottom-color: var(--accent); color: var(--fg); font-weight: 600; }
.image-search { display: flex; gap: 0.5rem; max-width: 36rem; margin-bottom: 0.75rem; }
.image-search input {
  flex: 1;
  min-width: 0;
  padding: 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: transparent;
  color: inherit;
  font: inherit;
}
.image-search input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.image-search select { flex: none; width: auto; }
/* The files as tiles, the chosen first, each ticked in its corner. */
.image-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
  gap: 0.75rem;
  max-width: 60rem;
  margin: 0 0 0.75rem;
  padding: 0;
  list-style: none;
}
.image-grid.chosen { padding-bottom: 0.75rem; border-bottom: 1px dashed var(--border); }
.image-tile { position: relative; display: block; cursor: pointer; }
.image-tile img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border: 2px solid transparent;
  border-radius: var(--radius);
  background: var(--subtle);
}
.image-tile:has(input:checked) img { border-color: var(--accent); }
.image-tick { position: absolute; top: 0.35rem; left: 0.35rem; line-height: 0; }
.image-about { display: block; margin-top: 0.2rem; }
.image-pages { margin: 0 0 1rem; }
/* A chosen file, taken away by the cross in its corner. */
.image-remove {
  position: absolute;
  top: 0.35rem;
  right: 0.35rem;
  width: 1.6rem;
  height: 1.6rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgb(0 0 0 / 60%);
  color: #ffffff;
  line-height: 1;
}
.image-remove:hover { background: rgb(0 0 0 / 80%); }
/* The pictures chosen, each in the shape the ads have it: a file in its own,
   a feed's picture in the shape it is cut to -- or its own, taken as it is.
   All as high as each other, as wide as their shape makes them, in a row. */
.image-grid.chosen-pictures { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 1rem; }
.chosen-pictures .image-tile img,
.chosen-pictures .image-feed-none { width: auto; height: 7rem; max-width: 16rem; aspect-ratio: auto; }
.chosen-pictures .image-remove { top: 0.3rem; right: 0.3rem; width: 1.4rem; height: 1.4rem; font-size: 0.9rem; }
.chosen-pictures .image-about { margin-top: 0.35rem; white-space: nowrap; }
.row-preview .preview-stage { padding: 1rem; }
.image-feed-none {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  aspect-ratio: 1;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--fg) 5%, var(--surface));
}
/* The picker's upload: a button that opens the file dialog, the file input
   itself out of sight inside it. */
.image-upload { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; }
.image-upload input[type="file"] { display: none; }

/* An audience a line, what it is on the left and how it is used on the right. */
.segments { list-style: none; margin: 0 0 1rem; padding: 0 0.5rem 0 0; max-width: 40rem; max-height: 28rem; overflow-y: auto; }
.segments li { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.4rem 0; }
.segments li + li { border-top: 1px solid var(--border); }
.segments label { display: flex; flex-direction: column; min-width: 0; font-weight: normal; }
.segments select { flex: none; width: auto; }
.location-search { display: flex; gap: 0.5rem; max-width: 32rem; }
.location-choices { max-width: 32rem; margin-bottom: 1rem; }

/* Choosing which columns the campaigns table shows, and in what order. */
/* The Columns dialog: a column per row, a tab per column of boxes. The names
   stand apart at the top, since they do not move. */
/* Laid out from the heading row, so the three tabs' columns are one width
   whatever the names beside them, and a long name wraps rather than pushing
   the boxes off a narrow screen. */
.column-matrix { width: 100%; table-layout: fixed; border-collapse: collapse; margin: 0 0 1rem; }
.column-matrix th, .column-matrix td { padding: 0.25rem 0.375rem; }
.dialog-columns { width: min(36rem, calc(100vw - 2rem)); }
.dialog-columns .form { max-width: none; }
.column-matrix thead th:not(:first-child) { width: 6rem; white-space: nowrap; }
@media (max-width: 30rem) {
  .column-matrix th, .column-matrix td { padding: 0.25rem; }
  .column-matrix thead th { font-size: 0.75rem; }
  .column-matrix thead th:not(:first-child) { width: 4rem; }
}
/* The tabs' names stay in sight while the list scrolls under them, below the
   dialog's own sticky title. */
.column-matrix thead th {
  position: sticky;
  /* The title's height, less the dialog's padding it is pulled up into. */
  top: 3rem;
  z-index: 1;
  background: var(--bg);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--muted);
  text-align: center;
  box-shadow: inset 0 -1px 0 var(--border);
}
.column-matrix tbody th { font-weight: normal; text-align: left; overflow-wrap: anywhere; }
.column-matrix td { text-align: center; }
.column-matrix .column-names tr:last-child > * { border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
.column-matrix .column-names + tbody tr:first-child > * { padding-top: 0.5rem; }
.column-matrix .drag-handle, .column-matrix .drag-handle-space {
  display: inline-flex; width: 1rem; margin-right: 0.5rem; vertical-align: middle;
}
.column-matrix .drag-handle { cursor: grab; }
.column-matrix tr.dragging { opacity: 0.4; }
/* Save stays in reach at the foot of a list longer than the window. */
.column-matrix ~ .buttons {
  position: sticky;
  bottom: -1.25rem;
  margin-bottom: -1.25rem;
  padding: 0.75rem 0 1.25rem;
  background: var(--bg);
  box-shadow: 0 -1px 0 var(--border);
}

/* Someone invited has no picture yet; the space where one will be keeps the
   names in a line. */
.avatar-empty { background: var(--border); }
/* A row that cannot be picked keeps the place a tick box takes, greyed out. */
.select-cell input:disabled { opacity: 0.35; cursor: default; }
/* Something to do with one row alone sits at the end of it. */
.rows-list .row-end { margin-left: auto; }

/* The menu folded down to its icons, for somebody who wants the width for a
   table. A choice kept on the user and drawn with the page, so the menu
   arrives the width it will stay. Only beside the page: on a narrow screen
   the menu is a bar that folds away of its own, and the button has nothing
   to do there. */
@media (max-width: 40rem) {
  .sidebar-collapse { display: none; }
}
@media not all and (max-width: 40rem) {
  .app { transition: grid-template-columns 150ms ease; }
  .app[data-sidebar="collapsed"] { grid-template-columns: 4.25rem 1fr; }
  .app[data-sidebar="collapsed"] .sidebar { padding: 1rem 0.5rem; overflow-x: hidden; }
  /* The mark above the button that opens the menu again, both centred in a
     column the width of an icon. */
  .app[data-sidebar="collapsed"] .brand { flex-direction: column; padding: 0; gap: 0.75rem; }
  .app[data-sidebar="collapsed"] .brand-full { display: none; }
  .app[data-sidebar="collapsed"] .brand-mark { display: block; }
  /* Which team, and a list of long names to switch between: nothing an icon
     can stand for. */
  .app[data-sidebar="collapsed"] .team-slot { display: none; }
  .app[data-sidebar="collapsed"] .menu a { justify-content: center; padding: 0.5rem; }
  .app[data-sidebar="collapsed"] .menu-count { position: absolute; top: 0; right: 0; margin: 0; }
  /* Gone from sight but not from a screen reader, which still reads each
     link by its name. */
  .app[data-sidebar="collapsed"] .menu-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .app[data-sidebar="collapsed"] .account { padding: 0; display: flex; justify-content: center; }
  .app[data-sidebar="collapsed"] .account .user > span:not(.avatar) { display: none; }
  .app[data-sidebar="collapsed"] .account .user { margin: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .app { transition: none; }
}

/* The three ways of looking at the campaigns, above their table: the
   campaigns, the ad groups inside them, the ads inside those. A row of words
   with a line under the one on show, as a tab strip is drawn. */
.level-tabs ul {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.75rem;
  list-style: none;
  margin: 0 0 1rem;
  padding: 0;
  border-bottom: 1px solid var(--border);
}
.level-tab { display: flex; align-items: center; gap: 0.5rem; }
.level-tab > a {
  display: inline-block;
  padding: 0.5rem 0;
  margin-bottom: -1px;
  border-bottom: 2px solid transparent;
  color: var(--muted);
  text-decoration: none;
}
.level-tab > a:hover { color: var(--fg); }
.level-tab.current > a { color: var(--fg); font-weight: 600; border-bottom-color: currentColor; }
/* How many campaigns are ticked, and the way to let go of them. */
.level-chosen {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  padding: 0.1rem 0.35rem 0.1rem 0.55rem;
  border-radius: 999px;
  background: var(--nav-active);
  color: var(--accent);
  font-size: 0.8125rem;
  font-weight: 600;
  white-space: nowrap;
}
.level-chosen-clear { display: inline-flex; color: inherit; border-radius: 999px; }
.level-chosen-clear:hover { background: var(--border); }
.level-chosen-clear svg { width: 0.9rem; height: 0.9rem; }

/* The campaigns' ads, a page further down: a card each, as many to a row as
   the window holds. */
.campaign-previews { margin-top: 1.5rem; }
/* Wide enough for a search ad to be drawn as the campaign's own page draws
   it, two to a row beside the menu. */
.preview-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(32rem, 100%), 1fr)); gap: 1rem; }
.preview-tile {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-width: 0;
  padding: 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.preview-tile > header { min-width: 0; font-weight: 600; }
.preview-tile > header a,
.preview-tile > header .muted { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.preview-tile > header a { color: inherit; }
/* An ad group's card names its campaign on a line of its own under it, so the
   ad group's name, the shorter and the one the card is of, is not the one cut
   short. The mark stands by the first line. */
.preview-tile > header:has(.tile-names) { align-items: flex-start; }
.preview-tile > header:has(.tile-names) svg { margin-top: 0.3rem; }
.tile-names { display: flex; flex-direction: column; min-width: 0; }
.tile-names .muted { font-weight: normal; }
.tile-names a { align-self: flex-start; max-width: 100%; }
.preview-tile .preview-stage { padding: 1rem; }
/* A standing ad is held narrower than on its campaign's page: its picture is
   square, so its width is its height, and a row of cards is read down a page
   rather than one at a time. */
.preview-tile .fb-ad { max-width: 17rem; }

/* A page one window tall whose table is the part that scrolls: the campaigns
   and the tabs beside them. The heading, the search, the tabs, the bar and
   the pages stay where they are, the header row stays at the top of the
   rows, and only the rows move -- both ways, since the columns are wider
   than the window as often as the rows are longer. A table shorter than the
   window is as tall as it is, with the pages right under it.
   Only beside the menu: on a narrow screen the page scrolls as a page, the
   window being too short to hold much of a table under everything else. */
@media not all and (max-width: 40rem) {
  /* No taller than the window less the page's own padding above and below,
     so a long table's foot is where it was when the page was the window, and
     what is below it is reached by scrolling the page on past it. A short
     table is as tall as it is, and what is below it starts right under it. */
  /* Nothing clipped here: the rows shrink to fit and scroll within
     themselves, and a menu opened over a short table hangs out past its foot
     rather than being cut off by it. */
  .fill-screen {
    display: flex;
    flex-direction: column;
    max-height: calc(100vh - 3rem);
    max-height: calc(100dvh - 3rem);
  }
  .fill-screen > * { flex: none; }
  .fill-screen > .rows {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
  .fill-screen > .rows > * { flex: none; }
  .fill-screen > .rows > .table-scroll {
    flex: 0 1 auto;
    min-height: 0;
    overflow: auto;
  }
  .fill-screen .table-scroll thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    /* Its own edge underneath. A cell's border stays behind with the table
       when the cell sticks, and without one the half of a row sliding under
       reads as a second line of the heading. */
    box-shadow: inset 0 -1px 0 var(--border);
  }
  /* Where the header row and the name column cross, above both. */
  .fill-screen .sticky-first thead th:first-child { z-index: 3; }
  .fill-screen > .rows > .pager { margin: 0.75rem 0 0; }
}

/* The Edit menu's groups, each behind an item of its own. */
.menu-rule { margin: 0.25rem 0; border: 0; border-top: 1px solid var(--border); }
.submenu > summary { list-style: none; }
.submenu > summary::-webkit-details-marker { display: none; }
.submenu > summary .submenu-arrow { display: flex; margin-left: auto; color: var(--muted); }
.submenu > summary .submenu-arrow svg { width: 0.875rem; height: 0.875rem; }
.submenu[open] > summary { background: var(--row-hover); }
/* Under its name where the window is narrow, set in a little. */
.submenu-panel { padding-left: 1rem; }
/* Beside the menu where there is room: the menu hangs from the bar's right
   end, so a group opens to its left. The menu does not scroll then -- a panel
   that scrolls cuts off whatever sticks out of it -- and a group scrolls on
   its own if it has to. */
@media (min-width: 48rem) {
  .edit-menu .menu-panel { overflow: visible; max-height: none; }
  .submenu { position: relative; }
  .submenu-panel {
    position: absolute;
    top: -0.25rem;
    right: calc(100% + 0.25rem);
    min-width: 13rem;
    max-height: min(70vh, 30rem);
    overflow-y: auto;
    padding: 0.25rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
  }
}
/* The Edit menu is a list of things to do rather than of values to pick, and
   grows by a heading at a time; it is let run to most of the window before it
   scrolls, so that what is further down is seen to be there. */
.edit-menu .menu-panel { max-height: min(85vh, 44rem); }

/* A dialog for picking from a long list -- places, languages, audiences: the
   heading and what is typed to search stay at the top and the buttons at the
   bottom, and the list between them scrolls. Wide enough on a large screen
   for a place's whole name to sit on one line. Flex only while open: a dialog
   is hidden by its display, and setting one would show it shut. */
.dialog-picker {
  width: min(48rem, calc(100vw - 2rem));
  height: min(56rem, calc(100vh - 4rem));
  overflow: hidden;
}
.dialog-picker[open] { display: flex; flex-direction: column; }
.dialog-picker .dialog-header { position: static; flex: none; }
.picker-form { display: flex; flex-direction: column; flex: 1; min-height: 0; max-width: none; }
.picker-form > * { flex: none; }
.picker-form > .picker-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  margin-bottom: 1rem;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.picker-form .buttons { margin: 0; }
.picker-list .location-choices { max-width: none; margin: 0; }
.picker-list .ticks { margin: 0.5rem 0; }
/* A box of lines in such a dialog -- keywords -- fills the room the list
   would, and scrolls on its own. */
.picker-form > .picker-fill { flex: 1; min-height: 0; display: flex; flex-direction: column; margin-bottom: 1rem; }
.picker-fill textarea { flex: 1; min-height: 8rem; max-height: none; field-sizing: fixed; resize: none; }
.picker-fill > p { margin: 0.35rem 0 0; }
/* Where the window is narrow, the Edit menu hangs under the whole bar, from
   edge to edge, rather than from its button: the button stands first among
   the bar's, and a menu hung from it would run off the window's left edge. */
@media (max-width: 47.99rem) {
  .rows-bar:has(.edit-menu) { position: relative; }
  .edit-menu { position: static; }
  .edit-menu .menu-panel { left: 0; right: 0; }
}
/* The Feed dialog of the ticked campaigns: Close stands apart from the last
   line, as it does at the end of the campaign page's Feed row. */
.bulk-feed .buttons { margin-top: 1.5rem; }
.picker-list > .placements-choice { padding-top: 0.5rem; }
/* It holds nothing to focus, so the dialog itself takes the focus; a ring
   round a dialog that is only waiting says nothing. */
.dialog-loading:focus, .dialog-loading:focus-visible { outline: none; }
/* The ticked campaigns' pictures, in a dialog: the whole picture ticks it, a
   ticked one outlined, as in the image picker. */
.bulk-images label.media-thumb:has(input:checked) { outline: 3px solid var(--accent); outline-offset: -3px; }
.bulk-images .picker-list .media-grid { padding: 0.75rem 0; }
.dialog-picker.dialog-wide { width: min(64rem, calc(100vw - 2rem)); }
/* The search and filters over the Images dialog's grid, on a line of their
   own that wraps on a narrow window. */
.bulk-images-filters { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.75rem; }
.bulk-images-filters input[type="search"] { flex: 1 1 14rem; min-width: 0; padding: 0.4rem 0.6rem; border: 1px solid var(--border); border-radius: var(--radius); background: transparent; color: inherit; font: inherit; }
.bulk-images-filters select { flex: none; }
/* Half ticked: some of the campaigns show it, and saving leaves it so. */
.bulk-images label.media-thumb:has(input:indeterminate) { outline: 3px dashed var(--accent); outline-offset: -3px; }
/* The ticked campaigns' keywords or texts, a line each: its box, then the
   line to change, in a box as tall as the line wraps to. A line whose box is
   empty is struck through, as saving takes it away. */
.bulk-lines { list-style: none; margin: 0; padding: 0.5rem 0; }
.bulk-lines li { display: flex; align-items: flex-start; gap: 0.6rem; padding: 0.15rem 0; }
.bulk-lines li > input[type="checkbox"] { margin-top: 0.7rem; }
.bulk-lines textarea { flex: 1; min-width: 0; padding: 0.35rem 0.5rem; border: 1px solid var(--border); border-radius: var(--radius); background: transparent; color: inherit; font: inherit; line-height: 1.5; resize: none; field-sizing: content; }
.bulk-lines textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.bulk-lines li:has(input[type="checkbox"]:not(:checked):not(:indeterminate)) textarea { text-decoration: line-through; opacity: 0.6; }
.bulk-lines-count { flex: none; white-space: nowrap; margin-top: 0.5rem; }
.bulk-lines li:not(:has(input:indeterminate)) .bulk-lines-count { display: none; }
/* A button standing apart from Save and Cancel, at the far end of the row. */
.buttons .buttons-end { margin-left: auto; }
/* A row of texts listed a line each: the line opens the dialog it is changed
   in, the bin after it takes it away. */
.text-list { list-style: none; margin: 0 0 0.5rem; padding: 0; border-top: 1px solid var(--border); }
.text-list li { display: flex; align-items: center; gap: 0.25rem; border-bottom: 1px solid var(--border); }
.text-list-line {
  flex: 1;
  min-width: 0;
  padding: 0.5rem 0.4rem;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  overflow-wrap: anywhere;
  cursor: pointer;
}
.text-list-line:hover { background: var(--row-hover); }
.text-list-line > span { display: block; }
.text-list-example { margin-top: 0.1rem; }
/* A product's place among the campaign's, as narrow as its number. */
.products .product-place { width: 1%; white-space: nowrap; }
/* An example too long for the ad it is of: said beside it, amber as what
   needs doing is, and as a pill so it is not read as more of the example. */
.text-list-warning {
  display: inline-block; margin-left: 0.5rem; padding: 0 0.5rem; border-radius: 999px;
  background: var(--pending-bg); color: var(--pending-fg);
}
/* A table's column of tick boxes is as narrow as the box. */
th.select-column, td.select-column { width: 2.25rem; }
/* The search over a dialog's rows of lines, or of labels. */
.lines-search { margin-bottom: 0.75rem; }
.lines-search input { width: 100%; padding: 0.4rem 0.6rem; border: 1px solid var(--border); border-radius: var(--radius); background: transparent; color: inherit; font: inherit; }
.bulk-lines li[hidden], .label-choices li[hidden] { display: none; }
.picker-list .label-choices { margin: 0; padding: 0.5rem 0; }
/* A list changed since it was last written to its network. */
.badge.needs-sync, .badge.incomplete { background: var(--pending-bg); color: var(--pending-fg); }
.badge.sync-errors { background: var(--bad-bg); color: var(--bad-fg); }
.badge.needs-reconnect { background: var(--bad-bg); color: var(--bad-fg); }

/* What needs doing, at the top of the dashboard: a line a thing, each a way
   to where it is done. */
.attention {
  margin-bottom: 1.5rem; padding: 0.75rem 1rem; border-radius: 8px;
  background: var(--pending-bg); color: var(--pending-fg);
}
.attention ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 0.25rem; }
.attention a { color: inherit; font-weight: 600; }
/* On a campaign's page, under the overview, spaced from it as the groups are
   from each other. */
.takeover-body > .attention { margin: 1rem 0 0; }
/* What went wrong with the last send, where what it lacks would be: the same
   box, red rather than yellow. */
.attention.sync-failed { background: var(--bad-bg); color: var(--bad-fg); display: grid; gap: 0.4rem; }
.attention.sync-failed p { margin: 0; }
.attention.sync-failed ul { list-style: disc; padding-left: 1.25rem; }

/* A send going on, in place of the button that started it. */
.sync-running { display: inline-flex; align-items: center; gap: 0.4rem; }
.sync-running .spinner { width: 0.9rem; height: 0.9rem; }
