/*
  One stylesheet. The dashboard is six views of tabular data; a component
  framework would be more code than the thing it organises.
*/

:root {
  --bg: #0f1115;
  --panel: #171a21;
  --panel-2: #1e222b;
  --line: #2a2f3a;
  --text: #e6e8ec;
  --muted: #9aa3b2;
  --accent: #6aa9ff;
  --good: #4ade80;
  --warn: #fbbf24;
  --bad: #f87171;
  --radius: 10px;

  /* Scrollbars are chrome, not content. Translucent so they take the colour of
     whatever they sit on rather than being a bright bar against a dark panel,
     which is what the default looks like here. */
  --scroll: rgba(255, 255, 255, .13);
  --scroll-hover: rgba(255, 255, 255, .24);
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #f6f7f9; --panel: #fff; --panel-2: #f0f2f5; --line: #dfe3e9;
    --text: #12151a; --muted: #5b6472; --accent: #1d6ff2;
    --scroll: rgba(0, 0, 0, .16);
    --scroll-hover: rgba(0, 0, 0, .28);
  }
}

* { box-sizing: border-box; }

/* Firefox and the modern standard. `thin` is roughly half the default width. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--scroll) transparent;
}

/* WebKit and Blink, which ignore the above on older versions. */
::-webkit-scrollbar { width: 8px; height: 8px; }

/* No track at all: a visible groove doubles the weight of the thing and there
   is nothing to gain from marking where the bar could be. */
::-webkit-scrollbar-track { background: transparent; }

::-webkit-scrollbar-thumb {
  background: var(--scroll);
  border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover { background: var(--scroll-hover); }

/* Where two scrollable areas meet. Left as-is it renders as a pale square in
   the corner, which is more noticeable than either bar. */
::-webkit-scrollbar-corner { background: transparent; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

.loading { padding: 3rem; color: var(--muted); display: flex; align-items: center; gap: .6rem; }

/* "Something is happening", for the waits that have no fraction to report —
   a page fetching its data, a panel redrawing after a write. Drawn with a
   border so it needs no image and inherits the text colour around it. */
.spinner {
  display: inline-block;
  width: 1em;
  height: 1em;
  flex: none;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: spin .7s linear infinite;
  vertical-align: -.15em;
}

/* Inside a button, beside the label it replaced the idle text of. */
button .spinner { margin-inline-end: .4rem; }

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

/* Still says it is working, without turning: see the upload bar's note. */
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; border-top-color: currentColor; opacity: .45; }
}

/* ── shell ─────────────────────────────────────────────────────────────── */

/* One line, always.
   This used to wrap, and the header is sticky — so once the tabs needed a
   second row the whole bar grew taller and sat over the top of whatever was
   beneath it, including the controls people were reaching for. A scrolling row
   of tabs came next, and outgrew that too. So the pages now live in a menu
   behind one button, and the bar holds a fixed set of things whatever gets
   added to the dashboard. */
header.top {
  display: flex; align-items: center; gap: 1rem;
  padding: .75rem 1.25rem;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 10;
  flex-wrap: nowrap;
}

header.top h1 { font-size: 1rem; margin: 0; font-weight: 600; letter-spacing: .02em; }

header.top .spacer,
header.top select,
header.top button { flex-shrink: 0; }

/* The title gives up its width before anything functional does, and the name
   is the first thing to go entirely. Without this the bar simply overflowed on
   a phone once it could no longer wrap — trading two lines for a page that
   scrolls sideways is not a fix.
   `min-width: 0` is what lets these shrink at all: a flex item will not go
   below the width of its contents without it, and would push the header wider
   instead. */
header.top h1,
header.top .page-title,
header.top .muted {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

header.top .page-title { color: var(--muted); font-size: .9rem; }

@media (max-width: 700px) {
  /* Who you are signed in as is the least load-bearing thing here; Sign out
     stays, and that is what says a session is open. */
  header.top .muted { display: none; }
}

@media (max-width: 460px) {
  header.top { gap: .6rem; padding: .75rem .8rem; }
  /* The brand goes before the page name: on a phone, where you are is the
     thing the menu has hidden. */
  header.top h1 { display: none; }
}

/* Three bars drawn from one pseudo-element and its shadows, so the icon takes
   the text colour in either theme and there is no image to fetch. */
.menu-toggle { padding: .5rem .7rem; }
/* A line of text tall, so the button is exactly as tall as Sign out beside it. */
.menu-toggle span { display: block; position: relative; width: 1.05rem; height: 1.5em; }
.menu-toggle span::before {
  content: ''; position: absolute; left: 0; right: 0; top: 50%;
  height: 2px; margin-top: -1px; border-radius: 1px;
  background: currentColor;
  box-shadow: 0 -5px 0 currentColor, 0 5px 0 currentColor;
}
.menu-toggle[aria-expanded="true"] { background: var(--accent); border-color: var(--accent); color: #fff; }

/* Hangs from the header rather than the button, so it can sit flush with the
   window's right edge. The header is sticky, which already makes it the
   containing block. Its own scroll, because the list is the thing on this page
   most likely to keep growing, and a window is not always tall. */
nav.menu {
  position: absolute; top: 100%; right: 0;
  width: 260px;
  max-height: calc(100vh - 5rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex; flex-direction: column; gap: 2px;
  padding: .4rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-top: 0; border-right: 0;
  border-bottom-left-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, .35);
}
/* The attribute alone loses to `display: flex` above. */
nav.menu[hidden] { display: none; }

@media (max-width: 640px) {
  /* A 260px panel on a phone is a strip with a sliver of page beside it that
     nobody can use; the whole width is easier to hit. */
  nav.menu { left: 0; width: auto; border-left: 0; border-radius: 0; }
}

nav.menu a {
  color: var(--text); text-decoration: none;
  padding: .5rem .75rem; border-radius: var(--radius); font-size: .92rem;
}
nav.menu a:hover { background: var(--panel-2); }
nav.menu a.active { background: var(--accent); color: #fff; }

.spacer { margin-left: auto; }

main { padding: 1.25rem; max-width: 1200px; margin: 0 auto; }

/* ── panels and cards ──────────────────────────────────────────────────── */

.panel {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 1rem; margin-bottom: 1.25rem;
}

.panel h2 { margin: 0 0 .75rem; font-size: .95rem; font-weight: 600; }

.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: .75rem; }

.card { background: var(--panel-2); border-radius: var(--radius); padding: .8rem; }
.card .label { color: var(--muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .04em; }
.card .value { font-size: 1.5rem; font-weight: 600; margin-top: .2rem; }
.card .delta { font-size: .8rem; margin-top: .15rem; }

.flag-significant { color: var(--good); }
.flag-directional { color: var(--warn); }
.flag-noise { color: var(--muted); }

/* Where a contact came from, when it was not somebody typing them in. Quiet on
   purpose: it sits inside a name, and a badge that competes with the name has
   made the list harder to read in order to say something secondary. */
.tag {
  display: inline-block;
  margin-inline-start: .4rem;
  vertical-align: middle;
  padding: .05rem .4rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel-2);
  color: var(--muted);
  font-size: .68rem;
  font-weight: 500;
  letter-spacing: .02em;
  white-space: nowrap;
}

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

table { width: 100%; border-collapse: collapse; font-size: .9rem; }
th, td { text-align: left; padding: .45rem .5rem; border-bottom: 1px solid var(--line); }
th { color: var(--muted); font-weight: 500; font-size: .8rem; text-transform: uppercase; letter-spacing: .03em; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
tbody tr:hover { background: var(--panel-2); }

/* Hebrew section names sit next to English headers; per-cell direction is the
   only way both read correctly. */
td[dir="auto"] { unicode-bidi: plaintext; }

.bar { height: 6px; background: var(--panel-2); border-radius: 3px; overflow: hidden; }
.bar > span { display: block; height: 100%; background: var(--accent); }

/* ── A question with named answers ─────────────────────────────────────────
   Used where confirm() cannot be: a choice between two things that both do
   something, where the way out has to be neither of them. Each answer carries
   its own sentence, because "Resume" and "Start over" are only obvious to
   whoever wrote them. */
dialog.ask {
  color: var(--text); background: var(--panel);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1.1rem 1.2rem; max-width: 32rem; width: calc(100% - 2rem);
}

dialog.ask::backdrop { background: rgba(0, 0, 0, .55); }
dialog.ask h4 { margin: 0 0 .4rem; }
dialog.ask p { margin: 0 0 .9rem; }
dialog.ask menu { display: flex; flex-direction: column; gap: .5rem; margin: 0; padding: 0; }

/* Left-aligned and stacked: these are sentences, not toolbar buttons, and a
   row of them would put the hint under a button half its width. */
dialog.ask menu button { display: block; width: 100%; text-align: start; }
dialog.ask menu button small { display: block; margin-top: .15rem; opacity: .8; font-size: .82rem; }
dialog.ask menu button.quiet { background: none; border-color: transparent; color: var(--muted); }

/* askFor()'s fields. Stacked and full width for the same reason its buttons
   are: this is a form in a small box, not a toolbar. */
dialog.ask .ask-form { display: flex; flex-direction: column; gap: .7rem; }
dialog.ask .ask-form label { display: block; font-size: .88rem; }
dialog.ask .ask-form input { display: block; width: 100%; margin-top: .25rem; }
dialog.ask .ask-form small { display: block; margin-top: .2rem; }

/* A field that can be revealed: the input takes the room, the button sits
   beside it rather than under it, so showing a password does not move the
   thing being read. */
dialog.ask .ask-form .ask-field { display: flex; gap: .4rem; align-items: stretch; }
dialog.ask .ask-form .ask-field input { flex: 1; }
dialog.ask .ask-form .ask-field button.reveal { margin-top: .25rem; white-space: nowrap; }
dialog.ask .ask-form menu { flex-direction: row; gap: .4rem; }
dialog.ask .ask-form menu button { width: auto; text-align: center; }

/* ── forms, chat, misc ─────────────────────────────────────────────────── */

input, select, button, textarea {
  font: inherit; color: var(--text);
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--radius); padding: .5rem .65rem;
}

button { cursor: pointer; }
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
button:disabled { opacity: .55; cursor: default; }

.login { max-width: 340px; margin: 12vh auto; }
.login .panel > * + * { margin-top: .75rem; }
.login input { width: 100%; }
/* The service's public pages, one click from the sign-in form. */
.login-links { display: flex; justify-content: center; gap: .4rem 1.2rem; flex-wrap: wrap; margin-top: 1rem; font-size: .85rem; }
.login-links a { color: var(--muted); }
.login-links a:hover { color: var(--text); }

.error { color: var(--bad); font-size: .88rem; }
.muted { color: var(--muted); }
.empty { color: var(--muted); padding: 1.5rem 0; text-align: center; }

.chat-log { display: flex; flex-direction: column; gap: .75rem; margin-bottom: 1rem; }
.msg {
  padding: .6rem .8rem; border-radius: var(--radius); max-width: 46rem;
  white-space: pre-wrap;
  /* Each message decides its own direction from its content. An answer in
     Hebrew, a question in English and a quoted Hebrew section name all appear
     in one conversation. */
  unicode-bidi: plaintext;
}
.msg.user { background: var(--accent); color: #fff; align-self: flex-end; }
.msg.assistant { background: var(--panel-2); align-self: flex-start; }
.msg .tools { font-size: .75rem; color: var(--muted); margin-top: .4rem; }

/* The caveat line closes every answer. Set apart so it can be read at a glance
   or skipped, which is the whole point of it being one line at the end rather
   than hedging spread through the answer. */
.msg .caveat {
  unicode-bidi: plaintext;
  margin-top: .55rem;
  padding-top: .5rem;
  border-top: 1px solid var(--line);
  font-size: .85rem;
  color: var(--muted);
}

.chat-form { display: flex; gap: .5rem; }
.chat-form input { flex: 1; }

/* ── Pictures on a message ─────────────────────────────────────────────────
   Bounded rather than sized: what arrives is whatever somebody photographed,
   and a portrait held to a width is still four screens tall. Both dimensions
   are capped so any shape lands in the same envelope. */
.msg-media { display: block; margin-bottom: .4rem; }
.msg-media img,
.msg-media video {
  max-width: min(100%, 18rem);
  max-height: 18rem;
  border-radius: var(--radius);
  /* The bubble is coloured; a picture with transparency, or a video letterboxed
     inside its own box, would otherwise sit on the accent and read as part of
     it. */
  background: var(--panel-2);
  object-fit: contain;
}

/* Saving one picture.
   In the stamp row rather than overlaid on the image, which was the first
   attempt: a control positioned against the picture's corner cannot actually
   find it. A block shrink-wrapping a photograph is sized from the image's
   intrinsic width, and `max-height` reduces what is drawn without reducing that
   box — so on any portrait photo the button drifts into the space beside it.
   The stamp row is already where a message's furniture lives, and it is right
   every time. */
.msg-save {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid currentColor;
  /* The row is quiet by design; this is one more quiet thing in it, not a
     call to action. */
  opacity: .75;
}
.msg-save:hover, .msg-save:focus-visible { opacity: 1; }

/* What is attached but not yet sent. Above the form rather than inside it, so
   the message box keeps its full width and the picture is not competing with
   it for room. */
.attachment {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .4rem .5rem;
  margin-bottom: .5rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
}
.attachment img,
.attachment video {
  width: 2.5rem;
  height: 2.5rem;
  object-fit: cover;
  border-radius: calc(var(--radius) / 2);
  background: var(--panel);
}
.attachment .muted { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── A picture on its way up ───────────────────────────────────────────────
   The same row as a finished attachment, with the wait drawn into it. A file
   picked and then nothing happening for twenty seconds is the case this is
   for: on a phone tethered to a slow connection, a 16 MB video is a real wait,
   and an interface with no answer for it reads as one that dropped the file. */
.attachment.uploading .muted { flex: 0 1 auto; max-width: 40%; }

.upload-bar {
  position: relative;
  flex: 1;
  min-width: 4rem;
  height: .4rem;
  border-radius: 999px;
  background: var(--panel);
  overflow: hidden;
}

.upload-fill {
  display: block;
  height: 100%;
  width: 0;
  border-radius: inherit;
  background: var(--accent);
  /* Fast enough to keep up with the events, slow enough that the bar moves
     rather than jumps between them. */
  transition: width .18s linear;
}

/* Reading the file off the disk has no fraction to report, so the bar says
   "something is happening" instead of claiming a number it does not have. */
.upload-bar.sweeping .upload-fill {
  width: 40%;
  transition: none;
  animation: upload-sweep 1.1s ease-in-out infinite;
}

@keyframes upload-sweep {
  from { transform: translateX(-100%); }
  to { transform: translateX(250%); }
}

.upload-state {
  color: var(--muted);
  font-size: .78rem;
  /* Fixed, so the row does not twitch as 9% becomes 10% becomes Finishing…. */
  min-width: 4.5rem;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* A sweep nobody asked for is motion in the corner of the eye for as long as
   the upload lasts. The bar still fills; it just stops travelling. */
@media (prefers-reduced-motion: reduce) {
  .upload-bar.sweeping .upload-fill { animation: none; width: 100%; opacity: .45; }
  .upload-fill { transition: none; }
}

/* Pictures being fetched back from the bridge.
   Above the conversation rather than in the menu it was started from: the
   answer arrives over minutes, and the place to report that is the thing being
   filled in. Same sweeping bar as a video conversion, because it is the same
   claim — something is happening and there is no fraction to give. */
.backfill {
  display: flex;
  align-items: center;
  gap: .6rem;
  flex-wrap: wrap;
  padding: .45rem .6rem;
  margin-bottom: .5rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
  font-size: .82rem;
}

/* The bar is the only part that should take the leftover width; the count and
   the sentence beside it are their own size. */
.backfill .upload-bar { flex: 0 1 8rem; }
.backfill .upload-state { min-width: 5.5rem; text-align: left; }
.backfill .muted { flex: 1 1 12rem; }
.backfill.done { background: var(--panel); }

/* A button that reads as a link: these sit inside a sentence, and a real
   button there would carry more weight than "dismiss this notice" deserves. */
.link-button {
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}
.link-button:hover { color: var(--text); }

.schedule-attach { display: flex; gap: .5rem; }

/* Sessions beside the conversation, stacking on a phone — the sidebar is the
   least important thing on a narrow screen and the first to give up its room. */
.chat-layout { display: grid; grid-template-columns: 15rem 1fr; gap: 1rem; align-items: start; }

@media (max-width: 760px) {
  .chat-layout { grid-template-columns: 1fr; }
  .sessions { max-height: 12rem; }
}

.sessions {
  display: flex; flex-direction: column; gap: .35rem;
  max-height: 70vh; overflow-y: auto;
}

.sessions .session {
  display: flex; flex-direction: column; gap: .15rem;
  text-align: left; padding: .5rem .6rem; background: var(--panel); border: 1px solid var(--line);
}

.sessions .session:hover { background: var(--panel-2); }
.sessions .session.active { border-color: var(--accent); }
.sessions .session .title { font-size: .88rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sessions .session .meta { font-size: .72rem; color: var(--muted); }

.chat-main { margin-bottom: 0; }

/* Projects, contacts, conversation. The two lists give up their width first
   on a narrow screen; the conversation is the thing being read. */
.contacts-layout {
  display: grid;
  grid-template-columns: 13rem 15rem 1fr;
  gap: 1rem;
  align-items: start;
}

@media (max-width: 1000px) {
  .contacts-layout { grid-template-columns: 1fr 1fr; }
  .contacts-layout .chat-main { grid-column: 1 / -1; }
}

@media (max-width: 640px) {
  .contacts-layout { grid-template-columns: 1fr; }
}

/* One ad's leads, beneath the Leads row it belongs to.
   Indented and marked rather than merely listed: these sit in the same column
   as the projects, and without something saying "inside Leads" a row named
   after an ad reads as a project somebody could file a contact into. */
.sessions .session.ad-folder {
  margin-left: .9rem;
  border-left: 2px solid var(--line);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.sessions .session.ad-folder .title { font-size: .82rem; }
.sessions .session.ad-folder .meta { font-size: .68rem; }
.sessions .session.ad-folder.active { border-left-color: var(--accent); }

/* Finding anyone, from anywhere.
   The column wrapper exists so the results panel is not inside `.sessions` —
   that is the scrolling box, and anything absolutely positioned within it is
   clipped by its own overflow the moment it is taller than the rows. */
.contact-column {
  display: flex;
  flex-direction: column;
  gap: .5rem;
  min-width: 0;
  position: relative;
}

.finder { position: relative; }

.finder input {
  width: 100%;
  padding: .4rem .5rem;
  font-size: .85rem;
}

/* Over the list rather than pushing it down: the rows underneath are the
   context somebody is searching from, and shoving them about on every
   keystroke is motion for nothing. */
.finder-results {
  position: absolute;
  top: calc(100% + .25rem);
  left: 0;
  right: 0;
  z-index: 30;
  max-height: 24rem;
  overflow-y: auto;
  padding: .25rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: 0 10px 28px rgba(0, 0, 0, .38);
}

/* Wide enough to be readable, because a result carries three things: who,
   their number, and where they live. In a 15rem column that is one line too
   many, so the panel is allowed past its edge. */
@media (min-width: 641px) {
  .finder-results { min-width: 22rem; }
}

.finder-note { padding: .5rem; margin: 0; font-size: .82rem; }

.finder-hit {
  display: flex;
  flex-direction: column;
  gap: .1rem;
  padding: .4rem .5rem;
  border-radius: var(--radius);
  text-decoration: none;
  color: inherit;
}

.finder-hit:hover,
.finder-hit.active { background: var(--panel-2); }

.finder-who { font-size: .88rem; }
.finder-meta { font-size: .74rem; color: var(--muted); }

/* Which folder it came from. Chips rather than a sentence: there may be two —
   a lead filed into a project belongs to both — and "Project · Summer sale"
   reads as one name with a dot in it. */
.finder-where { display: flex; flex-wrap: wrap; gap: .25rem; margin-top: .15rem; }

.finder-where span {
  padding: .05rem .35rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: .68rem;
  color: var(--muted);
}

/* Which questions an ad's leads get asked, and the sieve made from them.
   Above the list because both belong to the folder rather than to any one
   lead. */
.domain-bar {
  display: flex;
  flex-direction: column;
  gap: .4rem;
  padding: .5rem .6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
}

.domain-pick { display: flex; align-items: center; gap: .4rem; font-size: .8rem; }
.domain-pick select { flex: 1; min-width: 0; font-size: .82rem; }

.domain-filters { display: flex; flex-direction: column; gap: .35rem; }

.domain-filter { display: flex; flex-direction: column; gap: .15rem; font-size: .74rem; }
.domain-filter select, .domain-filter input { font-size: .8rem; padding: .25rem .35rem; }

/* Two numbers that are one answer, so they sit on one line and share it. */
.range-pair { display: flex; gap: .3rem; }
.range-pair input { flex: 1; min-width: 0; }

/* The same questions, on one lead, where they are answered.
   In the Activity tab beside the record of the call they were learned on. */
.lead-profile {
  padding: .6rem .7rem;
  margin-bottom: .7rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
}

.lead-profile h3 { margin: 0 0 .5rem; font-size: .85rem; font-weight: 600; }

/* Side by side while there is room: three short answers read as one profile
   rather than as a form to work down. */
.profile-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: .5rem;
}

.profile-field { display: flex; flex-direction: column; gap: .2rem; font-size: .76rem; }
.profile-field select, .profile-field input { font-size: .85rem; padding: .3rem .4rem; }

/* The domain editor, in Admin. Nested lists because the data is nested: a
   domain holds questions, a question holds options, and an option may sit under
   another one. Showing that shape is most of explaining it. */
.domain-block { padding: .6rem 0; border-top: 1px solid var(--line); }
.domain-block:first-child { border-top: 0; }
.domain-block-head { display: flex; align-items: baseline; gap: .6rem; }
.domain-block-head h3 { margin: 0; font-size: .95rem; }

.domain-fields, .domain-options { list-style: none; margin: .4rem 0; padding: 0; }
.domain-field { padding: .4rem 0; }
.domain-field-head { display: flex; align-items: baseline; gap: .5rem; font-size: .88rem; }

.domain-options { margin-left: .8rem; border-left: 2px solid var(--line); }
.domain-options li {
  display: flex;
  align-items: baseline;
  gap: .4rem;
  padding: .12rem .5rem;
  font-size: .82rem;
}

.add-domain, .add-field, .add-option {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
  margin: .3rem 0 .3rem .8rem;
}
.add-domain { margin-left: 0; }
.add-field input, .add-field select, .add-option input, .add-option select, .add-domain input {
  font-size: .82rem;
  padding: .25rem .4rem;
}
.add-field input[name="label"], .add-option input[name="label"], .add-domain input { min-width: 12rem; }

.sessions a.session { text-decoration: none; color: inherit; }
.chat-log { max-height: 62vh; overflow-y: auto; }

/* When a turn happened. Small, and on every message, so a conversation read
   later has a shape in time rather than being one undated block. */
.msg .stamp { font-size: .7rem; color: var(--muted); margin-top: .35rem; opacity: .8; }
.msg.user .stamp { color: rgba(255, 255, 255, .75); }

/* ── Contacts: faces, tabs, timeline, sequence ─────────────────────────────
   A contact is a person, and a list of people that shows only names reads as a
   spreadsheet. The picture is the fastest way to find somebody in a list, so it
   carries its own weight even at 28px. */

.avatar {
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  object-fit: cover;
  flex: 0 0 auto;
  background: var(--panel-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: .7rem;
  color: var(--muted);
  /* A face is content, not chrome: it must not be stretched by a parent grid. */
  aspect-ratio: 1;
}

.avatar.lg { width: 3rem; height: 3rem; font-size: 1.1rem; }
.avatar.initials { border: 1px solid var(--line); font-weight: 600; letter-spacing: .02em; }

.avatar-button { background: none; border: 0; padding: 0; cursor: pointer; line-height: 0; }
.avatar-button:hover .avatar { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Scoped to `.sessions` on purpose: `.sessions .session` lays every row out as
   a column, and it outranks a bare `.contact-row`. Without matching that
   specificity the direction stayed `column`, which centred the link, let it
   size to its own content, and so kept the ellipsis on the name and the meta
   line from ever applying — a long line overflowed the column at both ends
   instead of being trimmed at one. */
.sessions .contact-row { display: flex; flex-direction: row; align-items: center; gap: .4rem; }
.contact-row .contact-link {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex: 1 1 auto;
  min-width: 0;
  text-decoration: none;
  color: inherit;
}
.contact-text { min-width: 0; display: block; }
/* The name is the one thing that must not wrap the row into two lines. */
.contact-text .title, .contact-text .meta {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Inside the row's menu now, where it has room to be a readable width rather
   than the 6.5rem it was squeezed into beside a name. */
.file-into { width: 100%; font-size: .8rem; margin-top: .2rem; }
.file-into-item { display: block; padding: .3rem .4rem; font-size: .88rem; }

/* The row's menu opens *inside* the row, not over it.
   The header's can be an overlay because nothing clips it. This one hangs off
   a column that scrolls, and a scrolling box clips what escapes it: as an
   overlay the panel was cut off at the column edge and the items could not be
   reached at all. Opening in place costs the row a second line while it is
   open, which is the honest trade.
   `display: contents` is what makes that possible — it lets the summary stay
   on the name's line while the panel becomes a full-width sibling below. */
.sessions .contact-row { flex-wrap: wrap; }
/* With wrapping on, a flex line that does not fit wraps *before* it shrinks —
   so the link kept its natural width and pushed the ⋯ onto a line of its own.
   A zero basis makes the link fill what is left instead of asking for what it
   wants, which puts the name and the control back on one line. */
.contact-row .contact-link { flex-basis: 0; }
.contact-row .row-menu { display: contents; }
.contact-row .row-menu summary { padding: .2rem .35rem; font-size: .95rem; }
.contact-row .row-menu menu {
  position: static;
  order: 9;
  flex: 1 0 100%;
  min-width: 0;
  margin-top: .35rem;
  box-shadow: none;
  background: var(--panel-2);
}

.contact-head {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: .75rem;
  margin-bottom: .5rem;
}
.contact-head h2 { margin: 0; }
.contact-head p { margin: .15rem 0 0; font-size: .8rem; }
.contact-actions { display: flex; gap: .35rem; align-items: center; justify-content: flex-end; }

/* Everything a contact can be done to, behind one control.
   Six buttons beside a name is six buttons wide, and on anything narrower than
   a laptop the name was the part that got pushed out — which is the one thing
   on that header nobody can do without. */
.row-menu { position: relative; }
.row-menu summary {
  list-style: none;
  cursor: pointer;
  padding: .35rem .55rem;
  line-height: 1;
  font-size: 1.1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
  color: var(--muted);
  user-select: none;
}
.row-menu summary::-webkit-details-marker { display: none; }
.row-menu[open] summary { border-color: var(--accent); color: var(--text); }
.row-menu menu {
  position: absolute;
  right: 0;
  top: calc(100% + .3rem);
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: .25rem;
  margin: 0;
  padding: .3rem;
  min-width: 13rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: 0 8px 24px rgba(0, 0, 0, .35);
}
/* Left-aligned and full width: these are sentences to pick from, not a
   toolbar, and a centred "Remove from project" reads as a heading.

   Anchors are styled with the buttons because one item here is a download,
   which a link does without JavaScript. It reads as an item, so it looks like
   one. */
.row-menu menu button,
.row-menu menu a {
  width: 100%;
  text-align: start;
  background: none;
  border-color: transparent;
  font-size: .88rem;
}
.row-menu menu a {
  display: block;
  padding: .35rem .5rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  color: var(--text);
  text-decoration: none;
}
.row-menu menu button:hover,
.row-menu menu a:hover { background: var(--panel-2); border-color: var(--line); }

@media (max-width: 640px) {
  .contact-head { grid-template-columns: auto 1fr; }
  .contact-actions { grid-column: 1 / -1; justify-content: flex-start; }
}

.tabs { display: flex; gap: .25rem; border-bottom: 1px solid var(--line); margin-bottom: .75rem; }
.tabs .tab {
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  padding: .4rem .7rem;
  color: var(--muted);
  cursor: pointer;
}
.tabs .tab.active { color: var(--text); border-bottom-color: var(--accent); }

/* One column, oldest first. Messages and visits are deliberately the same
   shape: the point of the list is that they are one story. */
.timeline, .queued, .steps { list-style: none; margin: 0; padding: 0; }

/* The activity list scrolls in its own box, the way the conversation does.
   Two reasons, and the second is the point. A year of history under a form
   pushes the rest of the page out of reach; and because render() rebuilds this
   element, a fresh box always starts at its own top — which, now that the
   newest entry is first, is where the thing somebody opened this to see is.
   Left to the page's own scroll it would open wherever the last tab was. */
.timeline { max-height: 62vh; overflow-y: auto; }

.timeline .event, .queued li, .steps .step {
  display: grid;
  grid-template-columns: 8.5rem 1fr;
  gap: .5rem;
  padding: .5rem 0;
  border-bottom: 1px solid var(--line);
  align-items: baseline;
}

.queued li, .steps .step { grid-template-columns: 8.5rem 1fr auto; }
.steps .step.off { opacity: .5; }

/* The row whose contents are in the form below, so the two are obviously the
   same message rather than a coincidence of similar text. */
.steps .step.editing { background: var(--panel-2); box-shadow: inset 2px 0 0 var(--accent); }

/* The same mark on the timeline entry being corrected, and on the form doing
   the correcting, so it is never a mystery which line the box now belongs to. */
.timeline .event.editing { background: var(--panel-2); box-shadow: inset 2px 0 0 var(--accent); }
.schedule.editing { box-shadow: inset 2px 0 0 var(--accent); }

/* The step form is the schedule form for a project: same shape, same habits. */
/* The project log: one row per message, four columns that stay aligned down the
   page so a run of failures is a shape rather than something to read for. */
.log { list-style: none; margin: 0; padding: 0; }

.log-row {
  display: grid;
  grid-template-columns: 8.5rem 12rem 1fr auto;
  gap: .5rem;
  padding: .5rem 0;
  border-bottom: 1px solid var(--line);
  align-items: baseline;
}

.log-who { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.log-state { display: flex; gap: .4rem; align-items: baseline; flex-wrap: wrap; justify-content: flex-end; }
.log-state .muted { font-size: .78rem; }

.log-filters { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .5rem; }
.log-filters button.active { border-color: var(--accent); color: var(--accent); }

/* Where each lead has got to, above the list it filters. The same shape as the
   log filters, smaller: this lives in a 15rem column and has to wrap to two or
   three rows without looking broken. Every status is shown, including the ones
   at zero — a filter whose empty options disappear looks like a filter that
   lost them. */
.status-filter { display: flex; gap: .3rem; flex-wrap: wrap; padding: .4rem .4rem .5rem; }
.status-filter .chip { font-size: .75rem; padding: .2rem .4rem; }
.status-filter .chip.active { border-color: var(--accent); color: var(--accent); }

/* The same fact on one contact, where it is a decision rather than a filter. */
.status-pick { display: inline-flex; align-items: center; gap: .3rem; font-size: .8rem; }
.status-pick select { padding: .25rem .35rem; }

/* On a phone the four columns become two: when and who on one line, what and
   how it went on the next. */
@media (max-width: 760px) {
  .log-row { grid-template-columns: 1fr auto; }
  .log-row .event-body { grid-column: 1 / -1; }
}

/* Draft or running, and the button that moves between them. Loud enough that
   nobody has to go looking for whether a sequence is reaching people. */
.seq-state {
  display: flex;
  align-items: center;
  gap: .6rem;
  flex-wrap: wrap;
  padding: .6rem .75rem;
  margin-bottom: .75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
}
.seq-state .muted { flex: 1; min-width: 12rem; }
.seq-state.running { border-left: 3px solid var(--good); }
.seq-state.draft { border-left: 3px solid var(--muted); }

#step-form h4 { margin: 0 0 .25rem; }
#step-form textarea { width: 100%; }

/* A burst is one arrival, so it reads as one block: the rows after the first
   lose their divider and sit under the group's time rather than repeating it. */
.steps .step.burst-first { border-bottom: 0; padding-bottom: .15rem; }
.steps .step.burst-more { border-bottom: 0; padding-top: .15rem; padding-bottom: .15rem; }
.steps .step.burst-more + .step:not(.burst-more) { border-top: 1px solid var(--line); }
.steps .step.burst-more .event-body { position: relative; }
.steps .step.burst-more .event-body::before {
  content: '↳';
  position: absolute;
  left: -1rem;
  color: var(--muted);
}
.steps .step.burst-more .event-body { padding-left: 0; }

.event-when, .step-when { color: var(--muted); font-size: .78rem; white-space: nowrap; }
.event-what { display: flex; flex-wrap: wrap; gap: .4rem; align-items: baseline; }
.event-body { display: block; width: 100%; }

/* The visit rows carry a left edge so a run of messages and a visit are
   distinguishable at a glance rather than by reading each one. */
.timeline .event.visit { border-left: 2px solid var(--accent); padding-left: .5rem; }
.timeline .event.out .event-what > strong { color: var(--accent); }

/* The lead form is where the whole conversation started, so it gets an edge of
   its own rather than sharing the visit's. */
.timeline .event.lead { border-left: 2px solid var(--good); padding-left: .5rem; }

/* The answers, as a question/answer list. Two columns while there is room,
   because a form's questions are short and its answers are not. */
.lead-answers {
  display: grid;
  grid-template-columns: minmax(6rem, auto) 1fr;
  gap: .15rem .6rem;
  width: 100%;
  margin: .3rem 0 0;
}
.lead-answers dt { color: var(--muted); font-size: .78rem; }
.lead-answers dd { margin: 0; overflow-wrap: anywhere; }

.queued-actions { display: flex; gap: .3rem; }

.schedule { display: grid; gap: .5rem; margin-bottom: 1rem; }
.schedule label { display: grid; gap: .2rem; font-size: .82rem; color: var(--muted); }
.schedule-when { display: flex; gap: .75rem; flex-wrap: wrap; }

/* Update job: pages of text, so the box is tall and grows only downwards. */
#job-body { width: 100%; min-height: 14rem; resize: vertical; line-height: 1.45; }
#job-form .schedule-attach { align-items: center; }
.capacity { display: flex; gap: .5rem; flex-wrap: wrap; align-items: baseline; margin: 0; }

@media (max-width: 640px) {
  .timeline .event, .queued li, .steps .step { grid-template-columns: 1fr; }
  .lead-answers { grid-template-columns: 1fr; gap: 0 0; }
  .lead-answers dd { margin-bottom: .3rem; }
  .queued-actions { justify-content: flex-start; }
}

/* A field label with an (i) that explains where to get the value —
   see labelWithHint() in pages/admin.js.

   `.schedule label` is display:grid, so the label text and the icon have to be
   one element or they land on separate rows. That is what .field-label is for. */
.field-label { display: inline-flex; align-items: center; gap: .1rem; }

.field-hint-wrap { position: relative; display: inline-flex; }

/* A real <button>, so its own resets have to be undone — the panel's own
   button styling would otherwise make a 32px pill out of a 1em circle. */
.field-hint {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.25em;
  height: 1.25em;
  min-width: 0;
  margin-left: .35em;
  padding: 0;
  background: none;
  border: 1px solid var(--muted);
  border-radius: 50%;
  color: var(--muted);
  font-size: .8em;
  font-style: italic;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  user-select: none;
}

.field-hint:hover,
.field-hint:focus-visible {
  border-color: var(--accent);
  color: var(--accent);
  outline: none;
}

/* Open is a state now, not a hover, so it stays lit while the panel is up. */
.field-hint[aria-expanded="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

/* Opened by click, closed by the ×, by Escape, or by clicking away — see
   wireHints() in pages/admin.js. Hover was wrong for this content: the token
   hint is a list of permission names to copy out, and a panel that disappears
   when the pointer leaves cannot be selected from.

   No overflow:hidden on .panel, so it is free to escape the form it sits in. */
.field-hint-pop {
  display: none;
  position: absolute;
  top: 1.9em;
  left: -.5rem;
  z-index: 40;
  width: min(30rem, 78vw);
  padding: .8rem .85rem .7rem;
  background: var(--panel-2, var(--panel));
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgb(0 0 0 / .35);
  color: var(--text);
  font-size: .78rem;
  font-weight: 400;
  line-height: 1.45;
  text-align: left;
  white-space: normal;
  cursor: auto;
  /* The reason this feature exists: the text has to be selectable. Inherited
     `user-select: none` from the icon would silently prevent copying. */
  user-select: text;
}

.field-hint-wrap.open .field-hint-pop { display: block; }

.field-hint-close {
  position: absolute;
  top: .3rem;
  right: .35rem;
  width: 1.5rem;
  height: 1.5rem;
  min-width: 0;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 4px;
  color: var(--muted);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}

.field-hint-close:hover,
.field-hint-close:focus-visible { background: var(--panel); color: var(--text); outline: none; }

.field-hint-pop strong { display: block; margin: .5rem 0 .2rem; color: var(--text); }
.field-hint-pop strong:first-child { margin-top: 0; padding-right: 1.4rem; }
.field-hint-pop p { margin: .25rem 0; color: var(--muted); }
.field-hint-pop ul { margin: .2rem 0 .4rem; padding-left: 1.1rem; }
.field-hint-pop li { margin: .15rem 0; }
.field-hint-pop code {
  padding: 0 .2em;
  background: var(--panel);
  border-radius: 3px;
  font-size: .95em;
}

/* Right-aligned on a narrow screen, where a popover anchored left would run
   off the edge of the viewport. */
@media (max-width: 560px) {
  .field-hint-pop { left: auto; right: -.5rem; width: min(30rem, 88vw); }
}

/* The ad builder's modal — see pages/adbuilder.js.
   Its own rather than ui.js's choose(): these forms have conditional rows, a
   file picker and a live preview, none of which a field list can express. */
.modal-host { position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; }

.modal-backdrop { position: absolute; inset: 0; background: rgb(0 0 0 / .55); }

.modal-card {
  position: relative;
  width: min(34rem, 92vw);
  /* Tall forms scroll inside the card rather than the page, so the buttons at
     the bottom stay reachable on a laptop screen. */
  max-height: 88vh;
  overflow-y: auto;
  padding: 1.1rem 1.2rem 1.2rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 20px 60px rgb(0 0 0 / .5);
}

.modal-card h2 { margin: 0 0 .8rem; font-size: 1rem; }

.modal-actions {
  display: flex;
  gap: .5rem;
  margin-top: .9rem;
  padding-top: .8rem;
  border-top: 1px solid var(--line);
}

/* A hidden conditional row must not leave its grid gap behind — an ad set form
   with the Page row hidden would otherwise have a visible hole in it. */
.modal-card label[hidden], .modal-card p[hidden] { display: none !important; }

.modal-card textarea {
  width: 100%;
  min-height: 4.5rem;
  padding: .4rem .5rem;
  background: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 6px;
  font: inherit;
  resize: vertical;
}

/* Update job: whether the job is live, which is what the ad API's sends obey. */
.job-active { display: flex; gap: .5rem; align-items: flex-start; font-size: .85rem; cursor: pointer; }
.job-active input { margin-top: .2rem; }
.job-active .muted { display: block; font-size: .78rem; }

/* The ad's API secret, on its card. Master only. */
.ad-secret { display: flex; flex-direction: column; gap: .25rem; font-size: .78rem; }
.ad-secret-row { display: flex; gap: .3rem; align-items: center; flex-wrap: wrap; }
#ad-secret-value { flex: 1; min-width: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .76rem; }

/* Update job: precision, 1 to 5, as one row of five equal steps. */
.job-precision { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin: 0; padding: 0; border: 0; font-size: .85rem; }
.job-precision legend { float: left; margin-right: .2rem; padding: 0; font-weight: 600; }
.precision-scale { display: inline-flex; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.precision-step { position: relative; cursor: pointer; }
.precision-step input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
.precision-step span { display: block; min-width: 2.2rem; padding: .3rem 0; text-align: center; font-variant-numeric: tabular-nums; }
.precision-step + .precision-step span { border-left: 1px solid var(--line); }
.precision-step input:checked + span { background: var(--accent); color: #fff; font-weight: 600; }
.precision-step input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: -2px; }

/* ── Alerts page ───────────────────────────────────────────────────────── */

.alert-group {
  border: 1px solid var(--line); border-radius: var(--radius);
  margin: 0 0 1rem; padding: .4rem .9rem .6rem;
}
.alert-group legend { font-weight: 600; padding: 0 .3rem; }

/* Text on the left, the two switches on the right; they stack on a phone. */
.alert-kind {
  display: flex; gap: .6rem 1rem; align-items: center; justify-content: space-between;
  flex-wrap: wrap; padding: .45rem 0; border-top: 1px solid var(--line);
}
.alert-kind:first-of-type { border-top: 0; }
.alert-kind-text { flex: 1 1 16rem; min-width: 0; }
.alert-kind strong { display: block; font-weight: 500; overflow-wrap: anywhere; }
.alert-kind .muted { font-size: .85rem; }

.alert-switches { display: flex; gap: 1rem; flex-shrink: 0; }
.alert-switches label { display: flex; gap: .3rem; align-items: center; font-weight: normal; cursor: pointer; white-space: nowrap; }
.alert-switches label:has(input:disabled) { opacity: .5; cursor: default; }

.alert-campaigns { max-height: 24rem; overflow-y: auto; }
#campaign-filter { width: 100%; max-width: 22rem; margin-bottom: .6rem; }

.alert-actions { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }

/* Social posts publishing — an imported day, one card per post. */
/* Three posts side by side on a wide screen, two on a narrower one, one on a phone. */
/* Newest first, in one row that scrolls sideways; one under another on a phone. */
.pub-posts { display: flex; gap: 1rem; align-items: start; overflow-x: auto; scroll-snap-type: x proximity; padding-bottom: .6rem; }
.pub-posts > .pub-post { flex: 0 0 min(380px, 85vw); scroll-snap-align: start; }
/* Inside a row that scrolls sideways the card's header would stick to the
   row, not under the page's bar — so it stays put there; on a phone it sticks. */
@media (min-width: 721px) { .pub-posts .pub-post-head { position: static; } }
@media (max-width: 720px) {
  .pub-posts { display: grid; grid-template-columns: minmax(0, 1fr); overflow-x: visible; }
  .pub-posts > .pub-post { flex: none; }
}
.pub-post { padding: 1rem; border: 1px solid var(--line); border-radius: var(--radius); }
/* A post's header stays in view while its pictures scroll past: which post,
   its state, what goes live, and the button that approves it. */
.pub-post { position: relative; }
.pub-post-head { position: sticky; top: var(--pub-top, 0px); z-index: 5; background: var(--panel);
  margin: -1rem -1rem .75rem; padding: .7rem 1rem; border-bottom: 1px solid var(--line);
  border-radius: var(--radius) var(--radius) 0 0; display: grid; gap: .35rem; }
.pub-post-title { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.pub-post-title h3 { margin: 0; }
.pub-post-sub { font-size: .8rem; }
.pub-post-actions { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.pub-post-actions button { padding: .35rem .9rem; }
.pub-status { font-size: .8rem; color: var(--muted); }
.pub-badge.draft { color: var(--muted); }
.pub-locked-note { font-size: .8rem; margin: 0 0 .5rem; }
.pub-badge { font-size: .75rem; padding: .1rem .5rem; border-radius: 999px; border: 1px solid currentColor; }
.pub-badge.ok { color: var(--good); }
.pub-badge.review { color: var(--warn); }
.pub-warnings { list-style: none; padding: 0; margin: .3rem 0 .6rem; color: var(--warn); font-size: .85rem; }
.pub-variants { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .6rem; }
.pub-variant { font-size: .8rem; padding: .15rem .5rem; border-radius: 6px; background: var(--line); }
.pub-variant.missing { text-decoration: line-through; color: var(--muted); }
.pub-field { margin: .5rem 0; }
.pub-label { font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.pub-value { white-space: normal; overflow-wrap: anywhere; line-height: 1.5; }
.pub-value[dir="rtl"] { text-align: right; font-size: 1.02rem; }
.pub-more summary { cursor: pointer; color: var(--muted); font-size: .85rem; margin-top: .4rem; }
.pub-raw { white-space: pre-wrap; font-size: .8rem; background: var(--bg); padding: .5rem; border-radius: 6px; max-height: 16rem; overflow: auto; }

/* Review: each variant as big as the post, one under another. */
.pub-slots { display: grid; gap: 1rem; margin: .5rem 0 1rem; }
.pub-slot { border: 2px solid var(--line); border-radius: var(--radius); padding: .5rem; }
.pub-slot.chosen { border-color: var(--good); }
.pub-slot.removed { border-style: dashed; }
.pub-slot img, .pub-slot video { display: block; width: 100%; height: auto; border-radius: 6px; background: var(--bg); }
.pub-slot-bar { display: flex; align-items: center; justify-content: space-between; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
.pub-choose { display: flex; align-items: center; gap: .45rem; cursor: pointer; }
.pub-choose input { width: 1.1rem; height: 1.1rem; }
.pub-slot-actions { display: flex; gap: .35rem; flex-wrap: wrap; }
.pub-slot-actions button { font-size: .8rem; padding: .25rem .6rem; }
.pub-video { position: relative; cursor: pointer; }
.pub-play { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 4rem; height: 4rem;
  border-radius: 50%; border: 0; font-size: 1.4rem; background: rgba(0, 0, 0, .55); color: #fff; }
.pub-video.playing .pub-play { opacity: 0; transition: opacity .2s; }
.pub-video.playing:hover .pub-play { opacity: 1; }
.pub-progress { height: 6px; background: var(--line); border-radius: 3px; margin-top: .4rem; overflow: hidden; }
.pub-progress span { display: block; height: 100%; width: 0; background: var(--accent); transition: width .2s; }
.pub-edit { display: block; }
.pub-edit textarea { width: 100%; box-sizing: border-box; resize: vertical; font: inherit; line-height: 1.5; }
.pub-edit textarea[dir="rtl"] { text-align: right; }
.pub-edit textarea[readonly] { opacity: .75; }
.pub-edit-bar { display: flex; align-items: center; gap: .4rem; margin-bottom: .2rem; }
.pub-edit-bar .spacer { flex: 1; }
.pub-saved { font-size: .72rem; color: var(--muted); }
.pub-saved[data-tone="ok"] { color: var(--good); }
.pub-saved[data-tone="bad"] { color: var(--bad); }
.pub-count { font-size: .72rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.pub-count.over { color: var(--bad); font-weight: 600; }
.pub-emoji-btn { padding: .1rem .4rem; font-size: 1rem; line-height: 1.2; }
.pub-emoji-menu { position: fixed; z-index: 50; width: min(20rem, calc(100vw - 16px)); max-height: 18rem; overflow-y: auto;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: .5rem;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .25); }
.pub-emoji-group { display: flex; flex-wrap: wrap; gap: .1rem; margin-bottom: .35rem; }
.pub-emoji-group .pub-label { width: 100%; }
.pub-emoji-menu button { font-size: 1.25rem; line-height: 1; padding: .25rem; border: 0; background: none; cursor: pointer; border-radius: 6px; }
.pub-emoji-menu button:hover { background: var(--line); }
.pub-badge.approved { color: var(--good); border-color: var(--good); }
.pub-fb-choose { width: 100%; display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
.pub-fb-choose:empty { display: none; }
.pub-network { font-size: .82rem; display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.pub-network.ok { color: var(--good); }
.pub-network.bad, .pub-network-error { color: var(--bad); }
.pub-network button { padding: .3rem .8rem; }

/* Order notifications */
.ord-new { display: flex; gap: .5rem; margin-top: .8rem; flex-wrap: wrap; }
.ord-new input { flex: 1; min-width: 12rem; }
a[data-site].active { font-weight: 600; }
.ord-inline { display: inline-flex !important; align-items: center; gap: .35rem; margin-right: 1rem; }
.ord-transport { border: 1px solid var(--line); border-radius: var(--radius); padding: .6rem .8rem; }
.ord-transport legend { font-size: .8rem; color: var(--muted); padding: 0 .3rem; }
.ord-qr { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; margin-top: .5rem; }
.ord-qr img { background: #fff; padding: 8px; border-radius: 8px; }
.ord-actions { display: flex; align-items: center; gap: .6rem; }
.ord-actions .spacer { flex: 1; }
.ord-api { display: grid; gap: .5rem; margin-bottom: 1rem; }
.ord-copy { overflow-wrap: anywhere; }
.ord-example { white-space: pre-wrap; font-size: .8rem; background: var(--bg); padding: .6rem; border-radius: 6px; }
button.danger { color: var(--bad); }
.ord-msg { display: flex; gap: .5rem; flex-wrap: wrap; font-size: .85rem; }

/* Menu groups: a heading, and its pages indented under it. */
nav.menu .menu-group { display: block; padding: .5rem .9rem .15rem; font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
nav.menu a.sub { padding-left: 1.8rem; }

/* A read-only facts dialog (ui.js info()). */
dialog.info { max-width: 40rem; }
dialog.info h5 { margin: 1rem 0 .3rem; font-size: .75rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
dialog.info dl { display: grid; grid-template-columns: minmax(8rem, max-content) 1fr; gap: .3rem 1rem; margin: 0; }
dialog.info dt { color: var(--muted); }
dialog.info dd { margin: 0; overflow-wrap: anywhere; }
.pub-tt-check:empty { display: none; }

/* Social posts publishing › Configuration: one block per service, each marked
   in that service's own colour, with where it stands on the right. */
.pub-account-bar { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: 1rem; }
.pub-account-bar select { max-width: 100%; }
.pub-service { border-top: 3px solid var(--brand); --brand: var(--line); }
.pub-service[data-service="schedule"] { --brand: var(--accent); }
.pub-service[data-service="drive"] { --brand: #1fa463; }
.pub-service[data-service="facebook"] { --brand: #1877f2; }
.pub-service[data-service="instagram"] { --brand: #e1306c; }
.pub-service[data-service="tiktok"] { --brand: #fe2c55; }
.pub-service-head { display: flex; align-items: center; gap: .6rem; margin-bottom: .9rem; }
.pub-service-head h2 { margin: 0; font-size: 1.05rem; }
.pub-service-mark { width: .7rem; height: .7rem; border-radius: 50%; background: var(--brand); flex-shrink: 0; }
.pub-service[data-service="instagram"] .pub-service-mark { background: linear-gradient(45deg, #f9ce34, #ee2a7b 55%, #6228d7); }
.pub-service[data-service="tiktok"] .pub-service-mark { background: linear-gradient(135deg, #25f4ee, #fe2c55); }
.pub-state { margin-left: auto; font-size: .75rem; padding: .15rem .6rem; border-radius: 999px; border: 1px solid currentColor;
  color: var(--muted); max-width: 55%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pub-state.ok { color: var(--good); }
.pub-state.warn { color: var(--warn); }
.pub-state.bad { color: var(--bad); }
.pub-row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.pub-row .pub-label { min-width: 4.5rem; }
.pub-service .schedule { margin-bottom: 0; }
.pub-creds { margin-top: 1rem; padding-top: .8rem; border-top: 1px solid var(--line); }
.pub-creds > summary { cursor: pointer; font-size: .9rem; }
.pub-creds[open] > summary { margin-bottom: .7rem; }
.pub-note { margin: .8rem 0 0; font-size: .85rem; color: var(--muted); overflow-wrap: anywhere; }
.pub-note:empty { display: none; }
.pub-tt-check { margin-top: .6rem; }
.pub-service code { overflow-wrap: anywhere; }

/* A post's TikTok settings: chosen by a person, as TikTok requires. */
.pub-tt { margin: .6rem 0; padding: .7rem .8rem; border: 1px solid var(--line); border-left: 3px solid #fe2c55; border-radius: var(--radius); }
.pub-tt.confirmed { border-left-color: var(--good); }
.pub-tt p { margin: .3rem 0; }
.pub-tt-form { display: grid; gap: .5rem; margin-top: .4rem; }
.pub-tt-privacy { display: grid; gap: .2rem; font-size: .85rem; color: var(--muted); }
.pub-tt-allow { display: flex; gap: .4rem 1rem; flex-wrap: wrap; align-items: center; }
.pub-tt-allow .pub-label { width: 100%; }
.pub-tt-check { display: inline-flex; gap: .35rem; align-items: center; cursor: pointer; }
.pub-tt-check.off { opacity: .45; cursor: default; }
.pub-tt-commercial { display: grid; gap: .2rem; padding-left: 1.4rem; }
.pub-tt-commercial p { font-size: .8rem; margin: 0 0 .3rem; }
.pub-tt-consent { font-size: .82rem; color: var(--muted); }
.pub-tt details summary { cursor: pointer; color: var(--muted); font-size: .85rem; }
.pub-tt > button { margin-top: .4rem; }
.pub-tt .pub-tt-hint { font-size: .8rem; margin: .1rem 0 0 1.4rem; }

/* Sign up, and the link to it under Sign in. */
.login-alt { font-size: .88rem; color: var(--muted); text-align: center; }
.su-agree { display: flex; gap: .5rem; align-items: flex-start; font-size: .85rem; color: var(--muted); line-height: 1.4; }
.su-agree input { width: auto; margin-top: .2rem; }
/* A field people never see: only a form-filling script puts anything in it. */
.su-trap { position: absolute !important; left: -9999px; width: 1px; height: 1px; opacity: 0; }
.signups select { max-width: 16rem; }
.signup-actions { display: flex; gap: .4rem; flex-wrap: wrap; }

/* The (i) guide to connecting Facebook. */
.pub-help summary { cursor: pointer; color: var(--accent); font-size: .88rem; }
.pub-help ol { margin: .5rem 0 .2rem; padding-inline-start: 1.2rem; font-size: .85rem; line-height: 1.55; color: var(--muted); }
.pub-help li { margin: .35rem 0; }
.pub-help strong { color: var(--text); }
#fb-token { width: 100%; box-sizing: border-box; font-family: ui-monospace, monospace; font-size: .8rem; }

/* A post added by hand: its empty slot, and its own time to go out. */
.pub-slot-empty { display: grid; gap: .6rem; justify-items: start; padding: 1.2rem; border-style: dashed; }
.pub-slot-empty p { margin: 0; }
.pub-when { display: grid; gap: .45rem; margin: 0 0 .8rem; padding: .7rem .8rem; border: 1px solid var(--line); border-left: 3px solid var(--accent); border-radius: var(--radius); }
.pub-when label { display: grid; gap: .25rem; font-size: .85rem; }
.pub-when p { margin: 0; font-size: .82rem; }
.pub-when button.danger { justify-self: start; }
.pub-progress-text { margin: .3rem 0 0; font-size: .8rem; font-variant-numeric: tabular-nums; }

/* Above the row of posts: ‹ a scroll bar in step with the one below › */
.pub-posts-nav { display: flex; align-items: center; gap: .5rem; margin: 0 0 .4rem; }
.pub-posts-nav[hidden] { display: none; }
.pub-posts-nav button { flex: none; width: 2.2rem; height: 2.2rem; padding: 0; font-size: 1.3rem; line-height: 1; border-radius: 50%; }
.pub-posts-track { position: relative; flex: 1; height: 10px; border-radius: 5px; background: var(--line); cursor: pointer; touch-action: none; }
.pub-posts-thumb { position: absolute; top: 0; left: 0; height: 100%; border-radius: 5px; background: var(--muted); }
.pub-posts-track:hover .pub-posts-thumb { background: var(--text); }
@media (max-width: 720px) { .pub-posts-nav { display: none; } }

/* Ad banners */
.bn-crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; margin: 0 0 1rem; font-size: .95rem; }
.bn-sep { color: var(--muted); }
.bn-props { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: .8rem; margin-bottom: 1rem; }
.bn-prop { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius); padding: .7rem; display: flex; flex-direction: column; gap: .5rem; min-width: 0; }
.bn-prop-head { display: flex; align-items: center; gap: .5rem; }
.bn-prop-head input { flex: 1; min-width: 0; }
.bn-text { white-space: pre-wrap; overflow-wrap: anywhere; }
.bn-media { width: 100%; max-height: 320px; object-fit: contain; border-radius: 6px; background: #000; }
.bn-prop textarea, .bn-clip textarea, form[data-act="scenario"] textarea { width: 100%; box-sizing: border-box; }
.bn-replace { font-size: .85rem; color: var(--muted); }
.bn-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.bn-status { font-size: .85rem; }
.bn-add { border-top: 1px solid var(--line); padding-top: .8rem; margin-top: .4rem; display: flex; flex-direction: column; gap: .5rem; }
.bn-add h3 { margin: 0; font-size: .9rem; }
.bn-row { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
.bn-row input[type="text"], .bn-row input[type="password"] { flex: 1; min-width: 12rem; }
.bn-list { margin: 0 0 1rem; padding-left: 1.2rem; line-height: 1.8; }
.bn-namebar { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.bn-namebar input { min-width: 14rem; }
.bn-clip { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius); padding: .7rem; margin-bottom: .8rem; display: flex; flex-direction: column; gap: .5rem; }
.bn-clip label { display: flex; flex-direction: column; gap: .25rem; font-size: .85rem; color: var(--muted); }
.bn-clip-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .5rem; }
.bn-clip-head label { flex-direction: row; align-items: center; gap: .4rem; }
.bn-clip-head input { width: 5.5rem; }
.bn-clip-head .bn-clip-step { flex: 1; min-width: 12rem; }
.bn-clip-head .bn-clip-step input { width: auto; flex: 1; }
/* A clip folded to one line; dragged by that line to reorder. */
.bn-clip-row { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius); margin-bottom: .5rem; }
.bn-clip-row > summary { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem .7rem; padding: .55rem .7rem; cursor: pointer; list-style: none; }
.bn-clip-row > summary::-webkit-details-marker { display: none; }
.bn-clip-row > summary::before { content: '▸'; color: var(--muted); }
.bn-clip-row[open] > summary::before { content: '▾'; }
.bn-grip { cursor: grab; color: var(--muted); }
.bn-clip-name { flex: 1; min-width: 8rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bn-clip-row.dragging { opacity: .45; }
.bn-clip-row > .bn-clip { border: 0; border-top: 1px solid var(--line); border-radius: 0 0 var(--radius) var(--radius); margin: 0; }
.bn-order { margin-bottom: .6rem; }
.bn-page a, .bn-page a:visited { color: #fff; text-decoration: underline; text-underline-offset: 2px; }
@media (prefers-color-scheme: light) { .bn-page a, .bn-page a:visited { color: var(--text); } }
.bn-main-grid { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 1rem; align-items: start; }
.bn-side .panel { position: sticky; top: 1rem; }
.bn-docs { list-style: none; margin: 0 0 .8rem; padding: 0; display: flex; flex-direction: column; gap: .45rem; }
.bn-docs li { overflow-wrap: anywhere; }
@media (max-width: 860px) { .bn-main-grid { grid-template-columns: minmax(0, 1fr); } }
.bn-drive-link { font-size: .85rem; }

/* Choosing a post's picture or video from Drive */
dialog.ask.pub-drive { width: min(760px, 94vw); }
.pub-drive-path { font-size: .9rem; overflow-wrap: anywhere; }
.pub-drive-path a, .pub-drive a[data-folder] { color: var(--text); text-decoration: underline; }
.pub-drive-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: .6rem; max-height: 60vh; overflow: auto; margin-bottom: .8rem; }
.pub-drive-folder { grid-column: 1 / -1; padding: .35rem 0; }
.pub-drive-file { display: flex; flex-direction: column; align-items: stretch; gap: .3rem; padding: .4rem; text-align: start; }
.pub-drive-file img, .pub-drive-noimg { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 6px; background: var(--panel-2); }
.pub-drive-noimg { display: flex; align-items: center; justify-content: center; font-size: 2rem; }
.pub-drive-file span { overflow-wrap: anywhere; font-size: .85rem; }
.pub-drive-file small { font-size: .75rem; }

/* Choosing a video's cover frame */
dialog.ask.pub-cover { width: min(560px, 94vw); }
.pub-cover-video { display: block; width: 100%; max-height: 60vh; background: #000; border-radius: 8px; margin-bottom: .6rem; }
.pub-cover-line { display: flex; align-items: center; gap: .5rem; margin-bottom: .9rem; }
.pub-cover-line input[type="range"] { flex: 1; }
.pub-cover-time { font-variant-numeric: tabular-nums; min-width: 4.5em; text-align: end; }
dialog.ask.pub-cover menu { flex-direction: row; }
dialog.ask.pub-cover menu button { width: auto; }
.bn-assets { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: .6rem; }
.bn-assets-one { grid-template-columns: minmax(0, 420px); }
.bn-asset { border: 1px dashed var(--line); border-radius: 8px; padding: .5rem; display: flex; flex-direction: column; gap: .35rem; min-width: 0; }
.bn-asset .bn-media { max-height: 220px; }
.bn-asset-none { margin: .6rem 0; }
.bn-publish-bar { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: .8rem 0; }
.bn-sent { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .35rem; font-size: .9rem; }
.bn-sent li.ok { color: var(--good); }
dialog.ask.bn-publish { width: min(520px, 94vw); }
.bn-targets { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .8rem; }
.bn-targets label { display: flex; align-items: center; gap: .5rem; }
.bn-caption { display: flex; flex-direction: column; gap: .3rem; margin-bottom: .8rem; }
.bn-caption textarea { width: 100%; box-sizing: border-box; }
dialog.ask.bn-publish menu { flex-direction: row; }
dialog.ask.bn-publish menu button { width: auto; }
