/* ═══ tokens.css ═══ */
/*
 * Enso OnePage manager — internal console.
 *
 * Mobile first (B34): a chargé de compte opens this on a phone at a client's office. Every
 * colour below is checked against its background at AA or better — including the state colours,
 * which are the ones people read fastest and squint at hardest. #ee4351, the brand coral, is
 * 3.78:1 on white and therefore never carries text; #d92638 (4.90:1) does.
 */

/* Geist (SIL Open Font License, assets/fonts/OFL.txt), one variable file for every weight. The
   URL is relative to the stylesheet, which is served under /assets/<version>/ — so the font is
   versioned with everything else and cached the same way. */
@font-face {
  font-family: "Geist";
  src: url("fonts/Geist.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
}

:root {
  --font: "Geist", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
  /* One row, always: the client shell sticks right under it, so this may not depend on wrapping. */
  --topbar-h: 56px;

  --navy: #202e5e;
  --navy-deep: #16204a;
  --coral: #ee4351;          /* decorative fills only — fails AA for text */
  --coral-ink: #d92638;      /* the one that may carry text */
  --ink: #1a1f2e;
  --muted: #59607180;
  --muted-ink: #545c6e;
  --line: #e3e6ef;
  --paper: #f7f8fb;
  --card: #ffffff;

  --online: #0f7a3d;
  --preparing: #8a5a00;
  --blocked: #6d28d9;
  --error: #b42318;
  --unknown: #5b6577;

  --r: 12px;
  --r-sm: 8px;
  --shadow: 0 1px 2px rgba(26, 31, 46, .06), 0 4px 16px rgba(26, 31, 46, .05);
  --shadow-lift: 0 2px 6px rgba(26, 31, 46, .08), 0 12px 28px rgba(26, 31, 46, .10);
  --ease: cubic-bezier(.2, .7, .3, 1);
}

/* ═══ base.css ═══ */
* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--navy); }
a:hover { color: var(--coral-ink); }

:focus-visible {
  outline: 3px solid var(--coral-ink);
  outline-offset: 2px;
  border-radius: 4px;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: #fff;
  padding: 10px 16px;
  z-index: 100;
}
.skip:focus { left: 8px; top: 8px; }

/* ═══ topbar.css ═══ */
/* ── top bar ─────────────────────────────────────────────────────────────── */

/* ONE row, of a height the rest of the page can rely on: the client shell sticks right under
   it (`--topbar-h`). On a phone the bar used to wrap onto four rows; now the navigation scrolls
   sideways inside its row, like the tab bar below it does. */
.topbar {
  background: var(--navy);
  color: #fff;
  height: var(--topbar-h);
  padding: 0 24px;
  display: flex;
  align-items: center;
  gap: 12px;
  position: sticky;
  top: 0;
  z-index: 20;
}
.brand {
  font-weight: 800;
  font-size: 19px;
  letter-spacing: -.02em;
  color: #fff;
  text-decoration: none;
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex: none;
}
.brand b { font-weight: inherit; }
.brand span { color: var(--coral); }
.brand em {
  font-style: normal;
  font-weight: 500;
  font-size: 12px;
  opacity: .72;
  letter-spacing: .04em;
  text-transform: uppercase;
}
.topnav {
  display: flex;
  gap: 4px;
  margin-left: auto;
  align-items: center;
  min-width: 0;
}
/* The scroller is the strip of links, never the nav itself: the account menu's panel is
   positioned from the nav, and an overflow there clips it to the bar's height. */
.topnav-links { display: flex; gap: 4px; align-items: center; min-width: 0; overflow-x: auto; scrollbar-width: none; }
.topnav-links::-webkit-scrollbar { display: none; }
.topnav a, .topnav button {
  color: #fff;
  text-decoration: none;
  font-size: 14px;
  font-weight: 600;
  padding: 6px 12px;
  border-radius: 8px;
  border: 0;
  background: transparent;
  cursor: pointer;
  font-family: inherit;
  white-space: nowrap;
  transition: background .18s var(--ease);
}
.topnav a:hover, .topnav button:hover { background: rgba(255, 255, 255, .14); color: #fff; }
.topnav a[aria-current="page"] { background: rgba(255, 255, 255, .2); }
.whoami { font-size: 13px; opacity: .78; padding-left: 6px; }

@media (max-width: 700px) {
  .whoami, .brand em { display: none; }
  .topbar { padding: 0 12px; gap: 8px; }
  .topnav a, .topnav button { padding: 6px 10px; }
}
.envtag {
  font-size: 10px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .08em;
  background: var(--coral);
  color: #fff;
  border-radius: 999px;
  padding: 2px 8px;
  flex: none;
}

/* ═══ shell.css ═══ */
/* ── shell ───────────────────────────────────────────────────────────────── */

main { max-width: 1180px; margin: 0 auto; padding: 20px 16px 64px; }

.page-head {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 6px;
}
h1 { font-size: 26px; line-height: 1.2; margin: 0; letter-spacing: -.02em; }
h2 { font-size: 15px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted-ink); margin: 28px 0 12px; }
/* Inside a card the same title is the card's head: smaller, and flush with the top. */
.card > h2:first-child { margin-top: 0; font-size: 12px; letter-spacing: .08em; }
h3 { font-size: 16px; margin: 0 0 8px; }
p { margin: 0 0 12px; }
.lede { color: var(--muted-ink); margin: 0 0 18px; }
.right { margin-left: auto; }

/* ═══ client-head.css ═══ */
/* ── the client shell: identical above every tab ─────────────────────────── */

/* The shell sticks under the top bar, on the page's own background, so a long tab scrolls under
   the name, the state and the tabs. Inside `main`'s column, not full-bleed: a block wider than
   its parent widens the layout viewport and detaches every fixed bar below it. */
.client-shell {
  position: sticky;
  top: var(--topbar-h);
  z-index: 10;
  background: var(--paper);
  margin: -20px 0 20px;
  padding-top: 12px;
  border-bottom: 1px solid var(--line);
}
.client-shell .tabs { margin-bottom: 0; border-bottom: 0; }
.client-head-row { margin-bottom: 2px; }
.client-head-row h1 { font-size: 24px; }
.back-link { font-size: 13px; margin: 0 0 4px; }
.back-link a { color: var(--muted-ink); font-weight: 600; text-decoration: none; }
.back-link a:hover { color: var(--navy); }
/* Address, freshness, and the one gesture that changes the freshness. A client with no address
   yet prints neither a separator nor a dash — the missing address is a finding, not a field. */
.client-stamp {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 10px;
  color: var(--muted-ink);
  font-size: 13px;
  margin: 0 0 8px;
  min-height: 30px;
}
/* Any span after the first gets the separator: the host is now followed by an optional
   « adresse temporaire » chip, and tying the dot to one named sibling put it in front of the
   chip and nowhere else. */
.client-stamp > span + span::before { content: "·"; margin-right: 10px; }
/* The console is not reading the client's domain. Amber, because it is a state that ends by
   itself the day the DNS resolves, not a fault. */
/* No separator of its own: it is a chip, not another fact, and on a narrow screen the line
   wraps in front of it — leaving a dot alone at the start of a line, pointing at nothing. */
.client-stamp > span.stamp-temp::before { content: none; }
.stamp-temp {
  font-size: 12px;
  font-weight: 700;
  color: var(--preparing, #b98900);
  background: #fdf5e6;
  border-radius: 999px;
  padding: 2px 9px;
}
.temp-note { margin: 0 0 10px; max-width: 78ch; }
.client-stamp .inline-form { display: inline-flex; margin: 0; }
/* For a label a screen reader must hear and an eye must not see. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--shadow);
  padding: 16px;
}

/* ═══ motion.css ═══ */
/* ── entrances: light, once, and never in the way ────────────────────────── */

@keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.rise { animation: rise .32s var(--ease) both; }

/* Between two tabs of the same client the page changes by a paragraph and a table, and the white
   repaint in between is what makes the console feel slow: the server now answers every one of
   those tabs in three to fifteen milliseconds, and the flash is the only thing left that says
   otherwise. Two lines ask the browser to cross-fade the documents instead. A browser that does
   not know the at-rule navigates exactly as it did before, so this can only ever be an
   improvement or a no-op — and it is deliberately short, because a long fade IS slowness.

   The topbar keeps its own name so it does not fade against itself: it is identical on both
   sides of every navigation, and a header that blinks on every click reads as a reload. */
@view-transition { navigation: auto; }
::view-transition-old(root), ::view-transition-new(root) { animation-duration: 140ms; }
.topbar { view-transition-name: topbar; }
/* The client shell is the same on both sides of a tab change: named, it is transitioned as one
   element and stays put, instead of fading out and back in with the rest of the region. */
.client-shell { view-transition-name: client-shell; }
main.is-swapped .rise { animation: none; }

/* A tab that is fetched rather than navigated to keeps the page on screen while the answer is on
   its way. The region dims only if that takes longer than a glance — the delay is what keeps a
   fast answer (nearly all of them) from flickering. `aria-busy` is set by lib/nav.js. */
main[data-nav-target][aria-busy="true"] { opacity: .55; transition: opacity .2s var(--ease) .4s; }

@media (prefers-reduced-motion: reduce) {
  .rise, .shimmer::after { animation: none !important; }
  * { transition-duration: .01ms !important; }
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
}

/* ═══ buttons.css ═══ */
/* ── buttons ─────────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background: var(--navy);
  color: #fff;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  padding: 10px 16px;
  font: inherit;
  font-weight: 700;
  font-size: 14px;
  cursor: pointer;
  text-decoration: none;
  transition: background .18s var(--ease), transform .18s var(--ease), box-shadow .18s var(--ease);
}
.btn:hover { background: var(--navy-deep); color: #fff; box-shadow: var(--shadow); }
.btn:active { transform: translateY(1px); }
.btn[aria-busy="true"] { opacity: .65; cursor: progress; }
/* Disabled is not "busy": it must look inert, not like something already happening. */
.btn[disabled] { background: #c9ced9; color: #5b6577; cursor: not-allowed; box-shadow: none; }
.btn[disabled]:hover { background: #c9ced9; color: #5b6577; box-shadow: none; }
.btn-ghost { background: #fff; color: var(--navy); border-color: var(--line); }
.btn-ghost:hover { background: var(--paper); color: var(--navy); }
.btn-danger { background: var(--error); }
.btn-danger:hover { background: #8f1c13; }
.btn-sm { padding: 6px 12px; font-size: 13px; }
/* An icon button: square, quiet, and the spinner takes the icon's place while it works. It
   comes after `.btn` on purpose — same specificity, so the padding it undoes is the one above. */
.btn-icon { padding: 0; width: 30px; height: 30px; border-radius: 999px; gap: 0; }
.btn-icon .btn-label-idle { display: inline-flex; }
.btn-icon svg { display: block; }

.spinner {
  width: 14px; height: 14px;
  border: 2px solid rgba(255, 255, 255, .45);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin .7s linear infinite;
  display: none;
}
.btn-ghost .spinner { border-color: rgba(32, 46, 94, .3); border-top-color: var(--navy); }
[aria-busy="true"] .spinner { display: inline-block; }
[aria-busy="true"] .btn-label-idle { display: none; }
.btn-label-busy { display: none; }
[aria-busy="true"] .btn-label-busy { display: inline; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ═══ flash.css ═══ */
/* ── flash ───────────────────────────────────────────────────────────────── */

.flash {
  border-radius: var(--r-sm);
  padding: 12px 14px;
  margin-bottom: 16px;
  font-size: 14px;
  font-weight: 600;
  border: 1px solid;
}
.flash-ok { background: #eaf6ef; border-color: #bfe3cd; color: #0b5c2e; }
.flash-error { background: #fdecea; border-color: #f5c6c2; color: #8f1c13; }
/* Ce qui est passé quand même, et que l'opérateur doit savoir : une vérification qui n'a pas pu
   tourner ici n'est ni un succès ni un échec, et les deux autres niveaux mentent chacun sur un
   bout de ça. */
.flash-warn { background: #fdf5e6; border-color: #ecd9ae; color: var(--preparing); }

/* ═══ pills.css ═══ */
/* ── state pills ─────────────────────────────────────────────────────────── */

.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .01em;
  border-radius: 999px;
  padding: 3px 10px;
  border: 0;
  white-space: nowrap;
}
.pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.pill-online { color: var(--online); background: #eef8f2; }
.pill-preparing { color: var(--preparing); background: #fdf5e6; }
.pill-blocked { color: var(--blocked); background: #f3eefe; }
.pill-error { color: var(--error); background: #fdeceb; }
.pill-unknown { color: var(--unknown); background: #f1f3f7; }
/* The write mode, not a health state: written on the client's server, live, no deploy. */

/* ═══ chips.css ═══ */
/* ── filter chips ────────────────────────────────────────────────────────── */

.chips { display: flex; gap: 8px; flex-wrap: wrap; margin: 14px 0 18px; }
.chip {
  border: 1px solid var(--line);
  background: #fff;
  border-radius: 999px;
  padding: 7px 14px;
  font-size: 13px;
  font-weight: 700;
  color: var(--muted-ink);
  text-decoration: none;
  cursor: pointer;
  font-family: inherit;
  transition: border-color .18s var(--ease), color .18s var(--ease), background .18s var(--ease);
}
.chip:hover { border-color: var(--navy); color: var(--navy); }
.chip[aria-pressed="true"] { background: var(--navy); border-color: var(--navy); color: #fff; }
.chip .n { opacity: .7; font-variant-numeric: tabular-nums; }
/* Each state chip wears its state's colour at rest, so the row of filters is also the legend. */
.chip-online { color: var(--online); }
.chip-preparing { color: var(--preparing); }
.chip-blocked { color: var(--blocked); }
.chip-error { color: var(--error); }
.chip-maintenance { color: var(--unknown); }
.chip[aria-pressed="true"] .n { opacity: .85; }

/* ═══ search.css ═══ */
/* ── search ──────────────────────────────────────────────────────────────── */

.search {
  width: 100%;
  max-width: 340px;
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font: inherit;
  font-size: 15px;
  background: #fff;
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.search:focus { border-color: var(--navy); box-shadow: 0 0 0 3px rgba(32, 46, 94, .12); outline: none; }

/* ═══ fleet.css ═══ */
/* ── fleet grid ──────────────────────────────────────────────────────────── */

.grid {
  display: grid;
  gap: 14px;
  grid-template-columns: 1fr;
}
@media (min-width: 640px) { .grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .grid { grid-template-columns: repeat(3, 1fr); } }

.client-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  background: var(--card);
  border: 1px solid var(--line);
  border-left: 4px solid var(--unknown);
  border-radius: var(--r);
  box-shadow: var(--shadow);
  padding: 15px 16px;
  text-decoration: none;
  color: inherit;
  transition: transform .2s var(--ease), box-shadow .2s var(--ease), border-color .2s var(--ease);
}
.client-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); color: inherit; }
.client-card.s-online { border-left-color: var(--online); }
.client-card.s-preparing { border-left-color: var(--preparing); }
.client-card.s-blocked { border-left-color: var(--blocked); }
.client-card.s-error { border-left-color: var(--error); }
.client-card.s-unknown { border-left-color: var(--unknown); }

.client-top { display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap; }
.client-name { font-weight: 800; font-size: 17px; letter-spacing: -.01em; line-height: 1.25; }
.client-host { font-size: 13px; color: var(--muted-ink); word-break: break-all; }
/* The « adresse temporaire » chip rides beside the host, and a preview host is long: without
   this it broke mid-word (« adress / e temporaire ») and shoved the state pill onto its own
   line. */
.client-host .stamp-temp { display: inline-block; white-space: nowrap; word-break: normal; margin-left: 6px; }
.client-top .pill { margin-left: auto; flex-shrink: 0; }
/* Below this the name and the pill were squeezing each other; give the pill its own line. */
@media (max-width: 420px) {
  .client-top .pill { margin-left: 0; }
}

.facts { display: flex; gap: 16px; flex-wrap: wrap; font-size: 13px; color: var(--muted-ink); }
.fact b { display: block; font-size: 18px; color: var(--ink); font-variant-numeric: tabular-nums; font-weight: 800; }
/* AFTER `.fact b`, not before it: same specificity, so the one written last is the one that
   paints. `.fact-warn b` sat above and had been colouring nothing at all. */
.fact-warn b { color: var(--blocked); }
/* Zero published offers. The number is the statement now (the warning beside it was deleted),
   so it carries the colour AND its own weight rather than reading as an ordinary count. */
.fact-attn b { color: var(--preparing); }
.fact-attn { color: var(--preparing); font-weight: 600; }

.why { font-size: 13.5px; line-height: 1.45; margin: 0; }
.why-blocker, .why-warning { color: var(--ink); font-weight: 500; }
/* « +2 » : a count, not a sentence. Same chip on the row and on the card. */
.more { display: inline-flex; align-items: center; font-size: 12px; font-weight: 700; color: var(--muted-ink); background: #f1f3f7; border-radius: 999px; padding: 1px 8px; margin-left: 6px; font-variant-numeric: tabular-nums; }
.why-more { margin-top: -4px; }

.stamp { font-size: 12px; color: var(--muted-ink); margin-top: auto; padding-top: 4px; }

/* ── the parc as a list ──────────────────────────────────────────────────── */

.parc-head { align-items: flex-end; gap: 12px 16px; }
.parc-title { display: flex; flex-direction: column; gap: 2px; }
.parc-title .lede { font-size: 14px; }
.parc-tools { display: flex; align-items: center; gap: 8px 16px; flex-wrap: wrap; margin: 14px 0 16px; }
.parc-tools .chips { margin: 0; flex: 1; }
.parc-tools .search { max-width: 280px; padding: 8px 12px; font-size: 14px; }

/* A two-position switch: the pressed half is the navy the chips use for "on". */
.seg { display: inline-flex; gap: 2px; padding: 3px; border: 1px solid var(--line); border-radius: var(--r-sm); background: #fff; }
.seg button {
  font: inherit; font-size: 13px; font-weight: 600; color: var(--muted-ink);
  background: transparent; border: 0; border-radius: 6px; padding: 5px 10px; cursor: pointer;
  transition: background .18s var(--ease), color .18s var(--ease);
}
.seg button[aria-pressed="true"] { background: var(--navy); color: #fff; }

/* One row per client. The columns that are numbers are right-aligned and tabular so a column
   of thirty reads as a column; the row's link is the name, and the island makes the whole row
   follow it. */
.fleet-table { padding: 0; overflow: hidden; }
.fleet-table table { font-size: 14px; }
.fleet-table th { position: static; }
.fleet-table td { vertical-align: middle; padding: 11px 14px; }
.fleet-table .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.fleet-table td.num { font-weight: 600; }
.fleet-table td.num.is-warn { color: var(--preparing); font-weight: 700; }
.fleet-table td.stamp { font-weight: 400; font-size: 13px; }
.fleet-table .go { width: 28px; padding-left: 0; color: var(--muted-ink); font-size: 18px; line-height: 1; }
.fleet-row { cursor: pointer; transition: background .15s var(--ease); }
.fleet-row:hover { background: #fbfcfe; }
.fleet-who a { font-weight: 700; font-size: 15px; text-decoration: none; }
.fleet-who .client-host { display: block; font-size: 13px; }
.fleet-why { color: var(--ink); max-width: 44ch; }
.fleet-why .muted { color: var(--muted-ink); }

.fleet-list .grid { display: none; }
.fleet-cards .fleet-table { display: none; }
/* On a phone the cards ARE the list: a seven-column table does not fit, and the card already
   reads as a row. The switch goes with the table. */
@media (max-width: 719px) {
  .fleet .fleet-table, .seg-view { display: none; }
  .fleet .grid { display: grid; }
}

/* ═══ findings.css ═══ */
/* ── findings ────────────────────────────────────────────────────────────── */

/* One list, one row per finding: a mark that says the level, the sentence, what to do, and the
   way there. Rows in a card rather than a card per row: five red-edged boxes in a column read as
   five alarms, and a reader stops reading at the third. */
.findings { list-style: none; padding: 0; margin: 0; }
.finding {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px 16px;
  border-top: 1px solid var(--line);
  background: #fff;
}
.finding-body { flex: 1; min-width: 0; }
.finding h3 { font-size: 15px; margin: 0 0 2px; line-height: 1.35; }
.finding p { margin: 0; font-size: 13px; color: var(--muted-ink); }
.finding a { font-weight: 600; }
.finding-mark {
  flex: none;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  margin-top: 1px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  font-weight: 800;
  line-height: 1;
}
.finding-mark::before { content: "!"; }
.finding.l-blocker .finding-mark { background: #fdeceb; color: var(--error); }
.finding.l-warning .finding-mark { background: #fdf5e6; color: var(--preparing); }
.finding.l-info .finding-mark { background: #f1f3f7; color: var(--unknown); }
.finding-go { align-self: center; white-space: nowrap; font-size: 13px; font-weight: 600; text-decoration: none; }
/* On a phone the link goes UNDER the sentence, on its own line: beside it, it squeezed the text
   into a column six words wide. */
@media (max-width: 560px) {
  .finding { flex-wrap: wrap; }
  .finding-body { flex: 1 1 calc(100% - 38px); }
  .finding-go { flex: 0 0 100%; margin-left: 38px; align-self: auto; }
}

/* ═══ lists.css ═══ */
/* ── data lists ──────────────────────────────────────────────────────────── */

.kv { display: grid; grid-template-columns: 1fr; gap: 10px; }
@media (min-width: 560px) { .kv { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px) { .kv { grid-template-columns: repeat(4, 1fr); } }
.kv > div {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 12px 14px;
}
.kv dt { font-size: 12px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted-ink); font-weight: 700; }
.kv dd { margin: 4px 0 0; font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; }
.kv dd small { font-size: 13px; font-weight: 600; color: var(--muted-ink); }
@media (min-width: 560px) { .kv-2 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px) { .kv-2 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px) { .kv-3 { grid-template-columns: repeat(3, 1fr); } }
.kv-tight dd { font-size: 15px; font-weight: 700; }

/* ═══ health.css ═══ */
/* ── is the site well? one sentence, one colour, one shape ─────────────────
 * Three instrument rows (a status code, "l'API répond", a timestamp) became one verdict. The
 * colour is never the only signal: the sentence says it out loud and the mark has its own glyph
 * and its own outline, so the widget survives a colour-blind reader and a grey print.
 */
.health-block { display: grid; gap: 12px; align-items: start; }
@media (min-width: 900px) { .health-block { grid-template-columns: minmax(280px, 360px) 1fr; } }

.verdict {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: 12px;
  row-gap: 3px;
  background: var(--card);
  border: 1px solid var(--line);
  border-left-width: 5px;
  border-radius: var(--r-sm);
  padding: 14px 16px;
}
.verdict-mark {
  grid-row: 1 / -1;
  align-self: start;
  width: 22px;
  height: 22px;
  margin-top: 1px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  font-weight: 800;
  line-height: 1;
}
.verdict-say { margin: 0; font-weight: 700; font-size: 15px; line-height: 1.35; }
.verdict-meta { margin: 0; font-size: 13px; color: var(--muted-ink); }
.verdict-detail { margin: 0; font-size: 12.5px; color: var(--muted-ink); }

.verdict-ok { border-left-color: var(--online); }
.verdict-ok .verdict-mark { background: #eef8f2; color: var(--online); border-radius: 50%; }
.verdict-ok .verdict-mark::before { content: "\2713"; }

.verdict-warn { border-left-color: var(--preparing); }
.verdict-warn .verdict-say { color: var(--preparing); }
.verdict-warn .verdict-mark { background: #fdf5e6; color: var(--preparing); border-radius: 6px; }
.verdict-warn .verdict-mark::before { content: "!"; }

.verdict-down { border-left-color: var(--error); }
.verdict-down .verdict-say { color: var(--error); }
.verdict-down .verdict-mark { background: #fdeceb; color: var(--error); border-radius: 2px; }
.verdict-down .verdict-mark::before { content: "\2715"; }

.verdict-neutral { border-left-color: var(--unknown); }
.verdict-neutral .verdict-mark { background: #f1f3f7; color: var(--unknown); border-radius: 50%; border: 1px dashed #cfd4e0; }
.verdict-neutral .verdict-mark::before { content: "?"; }

table { width: 100%; border-collapse: collapse; font-size: 14px; background: #fff; }
.table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--r); box-shadow: var(--shadow); }
th, td { text-align: left; padding: 10px 14px; border-bottom: 1px solid var(--line); vertical-align: top; }
th { font-size: 12px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted-ink); background: var(--paper); position: sticky; top: 0; }
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: #fbfcfe; }
.mono { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; font-size: 13px; }
.nowrap { white-space: nowrap; }

.timeline { list-style: none; margin: 0; padding: 0; }
.timeline li { display: flex; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--line); font-size: 14px; }
.timeline li:last-child { border-bottom: 0; }
.timeline time { color: var(--muted-ink); white-space: nowrap; font-size: 13px; min-width: 108px; }

details.expert { margin-top: 14px; }
details.expert > summary {
  cursor: pointer;
  font-size: 13px;
  font-weight: 700;
  color: var(--muted-ink);
  padding: 8px 0;
  list-style: none;
}
details.expert > summary::-webkit-details-marker { display: none; }
details.expert > summary::before { content: "▸ "; }
details.expert[open] > summary::before { content: "▾ "; }

.empty {
  text-align: center;
  padding: 48px 16px;
  color: var(--muted-ink);
  border: 1px dashed var(--line);
  border-radius: var(--r);
  background: #fff;
}
.empty strong { display: block; font-size: 17px; color: var(--ink); margin-bottom: 6px; }

/* ═══ skeleton.css ═══ */
/* ── skeletons: shaped like what they stand in for ───────────────────────── */

.shimmer { position: relative; overflow: hidden; background: #eef0f6; border-radius: 6px; }
.shimmer::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .65), transparent);
  animation: sweep 1.3s infinite;
}
@keyframes sweep { to { transform: translateX(100%); } }
.sk-line { display: block; height: 12px; margin: 8px 0; }
.sk-title { height: 18px; width: 60%; }
.sk-pill { height: 22px; width: 84px; border-radius: 999px; }
.sk-sm { width: 55%; height: 10px; }
.sk-lg { width: 72%; height: 20px; }
/* A heading's placeholder carries a heading's margins, or the block below it starts higher than
   the block that replaces it. */
.sk-h2 { height: 20px; width: 42%; margin: 26px 0 12px; }
/* The shape of a `.btn-sm`, for a skeleton that stands in for a card with one on it. */
.sk-btn { display: block; flex: 0 0 auto; width: 118px; height: 30px; border-radius: var(--r-sm); margin: 8px 0 8px auto; }
/* The same height as the chart it stands in for, so nothing jumps when the real one lands. */
.sk-chart { height: 180px; border-radius: 8px; }
.sk-hint { margin-top: 12px; font-size: 13px; }

/* A window switch keeps the previous numbers on screen and dims them: blanking the panel to
   re-show a skeleton makes a fast answer flash, which reads as a glitch rather than as speed. */
[data-stats-panel] { transition: opacity .18s ease; }
[data-stats-panel].is-loading { opacity: .45; }
.stats-source, .offers-source { margin-top: 18px; font-size: 12px; }

/* ═══ offers.css ═══ */
/* ── what the client publishes: the offers, as a list somebody reads ────────
   Not a table. A chargé de compte comes here holding one question (« est-ce que mon annonce
   est bien en ligne ? »), so each row leads with the job title and carries the link that
   answers it; the reference, which nobody recognises by sight, is the last thing on the row.
   The rows are `.card` — the same surface as everywhere else, so a list of fourteen of them
   still reads as this console and not as a report pasted into it. */
.offers-count { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; margin: 0 0 12px; }
.offer-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.offer { padding: 14px 16px; transition: box-shadow .18s var(--ease), border-color .18s var(--ease); }
.offer:hover { box-shadow: var(--shadow-lift); border-color: #ccd3e6; }
/* The link sits ON the title row: it is the gesture of the row, and a phone thumb finds it
   there. It wraps under the title rather than squeezing it below ~220px. */
.offer-head { display: flex; align-items: flex-start; gap: 10px 12px; flex-wrap: wrap; }
.offer-title { flex: 1 1 220px; margin: 0; font-size: 16px; line-height: 1.35; }
.offer-head > .btn { flex: 0 0 auto; }
.offer-meta { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; margin: 6px 0 0; font-size: 13px; color: var(--muted-ink); }
.offer-sep { color: var(--muted); }
.offer-excerpt { margin: 8px 0 0; font-size: 14px; }
.offer-foot { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px; margin: 10px 0 0; font-size: 12.5px; color: var(--muted-ink); }
/* « il y a 3 h » is the fact a chargé de compte actually reads on this line, so it does not get
   the browser's default 80% on top of a line that is already small. */
.offer-foot small { font-size: inherit; color: var(--muted-ink); }
.offer-ref { font-size: 12px; }
/* Paging brings the top of the list back into view, and the top bar is sticky: `block: 'start'`
   put the count line at y=0, which is BEHIND it — on a phone the first job title landed entirely
   under the navy bar. The bar is 59px at 1440 and 91px when it wraps at 390. */
[data-offers-panel] { scroll-margin-top: 104px; }
/* Three columns rather than a centred flex row: on page one there is no « précédentes », and a
   label that recentres itself as you page makes the row twitch under the cursor that paged. */
.pager { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 10px; margin: 18px 0 0; }
.pager > [rel="prev"] { grid-column: 1; justify-self: start; }
.pager-at { grid-column: 2; text-align: center; font-size: 13px; color: var(--muted-ink); font-variant-numeric: tabular-nums; }
.pager > [rel="next"] { grid-column: 3; justify-self: end; }

/* Same as the usage panel: a page turn dims what is there instead of blanking it back to a
   skeleton, which would flash on a fast client and read as a glitch rather than as speed. */
[data-offers-panel] { transition: opacity .18s ease; }
[data-offers-panel].is-loading { opacity: .45; }

/* ═══ forms.css ═══ */
/* ── forms ───────────────────────────────────────────────────────────────── */

.form { display: grid; gap: 14px; max-width: 420px; }
.form label { display: block; font-size: 13px; font-weight: 700; }
.form input, .form select {
  width: 100%;
  margin-top: 5px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font: inherit;
  font-weight: 400;
  background: #fff;
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.form input:focus, .form select:focus {
  border-color: var(--navy);
  box-shadow: 0 0 0 3px rgba(32, 46, 94, .12);
  outline: none;
}
.hint { font-size: 12px; color: var(--muted-ink); font-weight: 400; margin-top: 4px; }

.login-shell { min-height: 100vh; display: grid; place-items: center; padding: 24px; }
.login-card { width: 100%; max-width: 400px; }
.login-card .brand { color: var(--navy); font-size: 24px; margin-bottom: 4px; }
.login-card .brand em { color: var(--muted-ink); opacity: 1; }

/* ═══ misc.css ═══ */
/* ── misc ────────────────────────────────────────────────────────────────── */

.notice {
  border: 1px solid #f2d9a8;
  background: #fdf7e9;
  border-radius: var(--r-sm);
  padding: 12px 14px;
  font-size: 14px;
  margin-bottom: 18px;
}
.notice strong { display: block; margin-bottom: 3px; }

footer {
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 16px 40px;
  font-size: 12.5px;
  color: var(--muted-ink);
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
}
footer a { color: var(--muted-ink); }

/* ═══ utilities.css ═══ */
/* ── utilities ───────────────────────────────────────────────────────────────
 * These exist because the Content-Security-Policy forbids inline styles, and a `style=""`
 * attribute is an inline style. Loosening the policy to `'unsafe-inline'` for the sake of a
 * dozen margins would have traded a real protection for a convenience.
 */

.m0 { margin: 0; }
.muted { color: var(--muted-ink); }
.meta { color: var(--muted-ink); font-size: 14px; }
.dd-sm { font-size: 15px; }
.inline-form { display: inline; }
.row { display: flex; gap: 6px; align-items: center; }
.pw-input { max-width: 150px; }
.why-more { color: var(--muted-ink); font-weight: 500; }
.brand-dark { color: var(--navy); }
.brand-dark span { color: var(--coral-ink); }
h2 + .card, h2 + .findings, h2 + .table-wrap, h2 + .kv, h2 + .empty { margin-top: 0; }
.card + h2 { margin-top: 28px; }
.chips + .table-wrap { margin-top: 4px; }
.empty + .empty { margin-top: 12px; }
/* The way out of a panel that could not load: a real address to the same content as a whole page,
   under the sentence that says what happened. */
.panel-failed .btn { margin-top: 12px; }
.table-wrap + .chips { margin-top: 16px; }
.card > h3 + p.mono { margin-bottom: 14px; }
.card h3 + .hint { margin-bottom: 6px; }
.error-page { margin-top: 40px; }

/* The staggered entrance was a per-card inline delay; nth-child says the same thing in CSS. */
.grid > .client-card:nth-child(1) { animation-delay: 0ms; }
.grid > .client-card:nth-child(2) { animation-delay: 30ms; }
.grid > .client-card:nth-child(3) { animation-delay: 60ms; }
.grid > .client-card:nth-child(4) { animation-delay: 90ms; }
.grid > .client-card:nth-child(5) { animation-delay: 120ms; }
.grid > .client-card:nth-child(6) { animation-delay: 150ms; }
.grid > .client-card:nth-child(7) { animation-delay: 180ms; }
.grid > .client-card:nth-child(n+8) { animation-delay: 210ms; }

/* ═══ tabs.css ═══ */
/* ── tabs ────────────────────────────────────────────────────────────────── */

.tabs {
  display: flex;
  gap: 2px;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--line);
  margin: 4px 0 20px;
}
.tab {
  padding: 9px 14px;
  font-size: 14px;
  font-weight: 700;
  color: var(--muted-ink);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color .18s var(--ease), border-color .18s var(--ease);
}
.tab:hover { color: var(--navy); }
.tab.is-active { color: var(--navy); border-bottom-color: var(--coral-ink); }
.tabs-sub { border-bottom: 0; margin-bottom: 8px; }
.tabs-sub .tab { padding: 6px 12px; font-size: 13px; border-radius: 999px; border-bottom: 0; }
.tabs-sub .tab.is-active { background: var(--navy); color: #fff; }

/* « Mise en route » is in the bar for every client, always. While the client is not live the
   tab wears a dot: the tab no longer has to appear and disappear to say there is work behind
   it. Decorative on purpose — the state pill above says the same thing in words. */
.tab.is-todo::after {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--preparing);
  margin-left: 6px;
  vertical-align: 1px;
}

/* ═══ publication.css ═══ */
/* ── publication banner ──────────────────────────────────────────────────── */

.pub {
  border: 1px solid var(--line);
  border-left: 4px solid var(--preparing);
  border-radius: var(--r);
  background: #fff;
  padding: 14px 16px;
  margin-bottom: 18px;
  box-shadow: var(--shadow);
  display: grid;
  gap: 6px;
}
.pub-published { border-left-color: var(--online); }
.pub-failed, .pub-conflict { border-left-color: var(--error); }
.pub-abandoned { border-left-color: var(--unknown); }
.pub-head { display: flex; align-items: center; gap: 10px; font-size: 15px; }
.pub-when { margin-left: auto; font-size: 12.5px; color: var(--muted-ink); }
.pub-actions { display: flex; gap: 14px; align-items: center; font-size: 13.5px; }
.pub p { font-size: 14px; }

.pub-spin {
  width: 13px; height: 13px;
  border: 2px solid rgba(138, 90, 0, .25);
  border-top-color: var(--preparing);
  border-radius: 50%;
  animation: spin .8s linear infinite;
  flex-shrink: 0;
}
.pub-inline { display: flex; align-items: center; gap: 7px; color: var(--preparing); font-weight: 600; }
.pub-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--unknown); margin-right: 6px; }
.pub-dot.pub-checking { background: var(--preparing); }
.pub-dot.pub-published { background: var(--online); }
.pub-dot.pub-failed, .pub-dot.pub-conflict { background: var(--error); }

.linkish {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-size: 13.5px;
  color: var(--muted-ink);
  text-decoration: underline;
  cursor: pointer;
}
.linkish:hover { color: var(--error); }

/* ═══ editor.css ═══ */
/* ── the editor ──────────────────────────────────────────────────────────── */

.editor { display: grid; gap: 16px; max-width: none; }
.fields { display: grid; gap: 18px; }
@media (min-width: 720px) { .fields { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.field label { display: block; font-size: 13px; font-weight: 700; }
.field input[type="text"], .field select, .field textarea {
  width: 100%;
  margin-top: 5px;
  padding: 9px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font: inherit;
  font-weight: 400;
  background: #fff;
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.field textarea { resize: vertical; min-height: 76px; }
.field input:focus, .field select:focus, .field textarea:focus {
  border-color: var(--navy);
  box-shadow: 0 0 0 3px rgba(32, 46, 94, .12);
  outline: none;
}
.field.has-problem input, .field.has-problem textarea { border-color: var(--error); }
.field-problem { color: var(--error); font-size: 12.5px; font-weight: 600; margin: 6px 0 0; }
.field .hint { margin-top: 5px; }
.field input::placeholder, .field textarea::placeholder { color: #98a0b3; }

.switch { display: flex; align-items: center; gap: 10px; font-weight: 600; cursor: pointer; }
.switch input[type="checkbox"] { width: 20px; height: 20px; accent-color: var(--navy); margin: 0; flex-shrink: 0; }

.colour { display: flex; align-items: center; gap: 8px; margin-top: 5px; }
.colour input[type="color"] {
  width: 40px; height: 38px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: #fff;
  cursor: pointer;
  flex-shrink: 0;
}
.colour input[type="text"] { margin-top: 0; }
.colour .linkish { flex-shrink: 0; }

.rows { display: grid; gap: 12px; margin-bottom: 12px; }
.agency-row { display: grid; gap: 8px; align-items: end; grid-template-columns: 1fr; }
@media (min-width: 640px) { .agency-row { grid-template-columns: 1fr 1fr auto; } }
.agency-row label { display: block; font-size: 13px; font-weight: 700; }
.agency-row input {
  width: 100%; margin-top: 4px; padding: 9px 12px;
  border: 1px solid var(--line); border-radius: var(--r-sm); font: inherit; font-weight: 400;
}

.form-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 4px; }

/* The one button, in reach: the form is long and the button used to be at its foot. Sticky
   inside the column, on the page's own background, so a reader mid-way down still sees it. */
.editor .form-actions { position: sticky; bottom: 0; z-index: 5; background: var(--paper); padding: 12px 0; margin-top: 8px; border-top: 1px solid var(--line); }

.hint-warn { color: var(--preparing); }


/* The agency lookup (B-agencies): a search, its hits, and the children an operator confirms
   before they are written into the rows below. Placed above those rows because it is what
   fills them, and because a uid typed by hand is the error this card exists to prevent. */
.agency-lookup { display: grid; gap: 12px; }
.agency-search { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; }
.agency-search .grow { flex: 1 1 260px; }
.agency-search label { display: block; font-size: 13px; font-weight: 700; }
.agency-search input {
  width: 100%; margin-top: 4px; padding: 9px 12px;
  border: 1px solid var(--line); border-radius: var(--r-sm); font: inherit; font-weight: 400;
}
/* The card is a grid, so a button in it would stretch edge to edge and read as the screen's
   primary action. The one primary action here is « Enregistrer », at the foot of the form. */
.agency-lookup > .btn { justify-self: start; }
/* `.btn` sets a display, which is an AUTHOR rule and therefore beats the `display: none` the
   browser gives `[hidden]`. Without this line the two staged buttons show from the start, so the
   card offers "pull the children" before an agency has been picked. Same fix as `.wire [hidden]`,
   and it has to come after buttons.css in the cascade, which it does. */
.agency-lookup [hidden] { display: none; }
.agency-hits { display: flex; gap: 8px; flex-wrap: wrap; }
.agency-hits:empty { display: none; }
.agency-hit { text-align: left; }

.agency-picks { display: grid; gap: 6px; }
.agency-picks:empty { display: none; }
.agency-pick {
  display: flex; gap: 10px; align-items: center;
  padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--card); font-size: 14px;
  transition: border-color .15s var(--ease), background-color .15s var(--ease);
}
.agency-pick:hover { border-color: var(--navy); }
/* Inactive is dimmed, never hidden: whether an emptied flag means "closed" or "never filled in"
   is unanswered upstream, and a row removed for a guess is a row nobody can put back. */
.agency-pick.is-inactive { background: var(--paper); }
.agency-pick.is-inactive span:not(.hint) { color: var(--muted-ink); }
.agency-pick .hint { margin-left: auto; }

.agency-status { margin: 0; font-size: 13px; color: var(--muted-ink); min-height: 1.2em; }
.agency-status.is-error { color: var(--error); }
.agency-status.is-unconfigured { color: var(--preparing); }
.agency-status.is-picked, .agency-status.is-loaded { color: var(--online); }

/* ═══ editor-preview.css ═══ */
/* ── preview ─────────────────────────────────────────────────────────────── */

.samples { display: grid; gap: 14px; }
@media (min-width: 760px) { .samples { grid-template-columns: repeat(2, 1fr); } }
.sample { border: 1px solid var(--line); border-radius: var(--r); background: #fff; overflow: hidden; box-shadow: var(--shadow); }
.sample-label {
  font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted-ink); padding: 10px 14px; border-bottom: 1px solid var(--line); background: var(--paper);
}
.sample-body { padding: 18px 16px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
/* The real widgets, inside the console. Their own stylesheet dresses them; this only gives them
   a column to sit in and a ground that is not the card's, so an unset colour is visible as
   unset instead of blending into the panel. */
.sample-live { display: grid; gap: 14px; align-items: stretch; background: #fbfbfd; overflow-x: auto; }
/* The widgets carry their own mobile rules; this only stops a long client label from pushing the
   console's own layout sideways. */
.sample-live > * { min-width: 0; }
.sample-arrow { padding: 10px 14px; }
.sample-live .enso-job-result { margin: 0; }
.sample-live .sample-row { flex-wrap: wrap; gap: 12px; align-items: center; }
.sample-live .sample-popup { max-width: 420px; padding: 14px 16px; border-radius: 10px; }
.sample-grow { flex: 1; }
.sample-body > * { font-size: 14px; font-weight: 700; border-radius: 8px; padding: 9px 15px; }
.s-cta { background: var(--s-cta); color: var(--s-btn-color); }
.s-btn { background: var(--s-btn-bg); color: var(--s-btn-color); border: 1px solid rgba(0, 0, 0, .12); }
.s-badge { background: var(--s-badge-bg); color: var(--s-badge-color); border-radius: 999px; font-size: 12px; padding: 5px 12px; }
.s-spont { background: var(--s-spont-bg); color: var(--s-spont-color); }
.s-chat { background: var(--s-chat-bg); color: var(--s-chat-color); }
.s-popup {
  background: var(--s-popup-bg); color: var(--s-popup-color);
  width: 100%; padding: 14px; border: 1px solid rgba(0, 0, 0, .1); font-weight: 600;
}

/* ═══ readability.css ═══ */
/* ── the readability score, under the rendering it is about ───────────────── */

/* One row per pair the plugin really paints. Numbers are tabular so a column of ratios reads
   as a column and does not shuffle sideways while somebody drags a picker. */
.a11y { border-top: 1px solid var(--line); padding: 12px 14px 14px; background: var(--paper); }
.a11y-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.a11y-title {
  font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted-ink);
}
.a11y-count {
  margin-left: auto; font-size: 13px; font-weight: 800; font-variant-numeric: tabular-nums;
  padding: 3px 10px; border-radius: 999px; transition: background .18s var(--ease), color .18s var(--ease);
}
.a11y-count.is-ok { color: var(--online); background: #eef8f2; }
.a11y-count.is-warn { color: var(--error); background: #fdeceb; }

.a11y-rows { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 2px; }
.a11y-row {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "name level" "colours ratio";
  gap: 2px 10px;
  align-items: baseline;
  padding: 7px 8px;
  border-radius: var(--r-sm);
  font-size: 13px;
}
/* Only the failing row is tinted: a list where every line is coloured is a list nobody scans. */
.a11y-row.is-fail { background: #fdeceb; }
.a11y-name { grid-area: name; font-weight: 700; }
.a11y-colours { grid-area: colours; font-size: 12px; color: var(--muted-ink); overflow-wrap: anywhere; }
.a11y-ratio { grid-area: ratio; font-variant-numeric: tabular-nums; font-weight: 700; text-align: right; }
.a11y-level {
  grid-area: level; justify-self: end; font-size: 11px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .04em;
  padding: 2px 9px; border-radius: 999px; white-space: nowrap;
}
.a11y-row.is-aaa .a11y-level { color: var(--online); background: #eef8f2; }
.a11y-row.is-aa .a11y-level { color: var(--online); background: #eef8f2; }
.a11y-row.is-fail .a11y-level { color: var(--error); background: #fff; }
.a11y-row.is-unknown .a11y-level { color: var(--unknown); background: #f1f3f7; }
.a11y-row.is-unknown .a11y-ratio { color: var(--muted-ink); font-weight: 600; }
/* Room for one line per pair as soon as there is room — the phone keeps the stacked form. */
@media (min-width: 620px) {
  .a11y-row {
    grid-template-columns: minmax(0, 1fr) auto auto auto;
    grid-template-areas: "name colours ratio level";
    gap: 12px;
  }
  .a11y-colours { text-align: right; }
  .a11y-ratio { min-width: 66px; }
  .a11y-level { min-width: 96px; text-align: center; }
}
.a11y .hint { margin: 10px 0 0; }

/* The two boxes behind an unreadable pair, marked where the hand already is. Not the error
   treatment: nothing was rejected and the form still publishes — it is an amber, not a red. */
.field.is-unreadable input[type="text"] { border-color: var(--preparing); }
.field.is-unreadable > label { color: var(--preparing); }

td.was { color: var(--muted-ink); text-decoration: line-through; }
td.now { font-weight: 700; }
.chip-static { cursor: default; font-size: 11px; padding: 2px 9px; white-space: nowrap; }

/* ═══ assets.css ═══ */
/* ── assets ──────────────────────────────────────────────────────────────── */

.dropzone-form { display: grid; gap: 12px; max-width: 620px; }
.dropzone {
  display: grid;
  gap: 6px;
  place-items: center;
  text-align: center;
  padding: 30px 20px;
  border: 2px dashed var(--line);
  border-radius: var(--r);
  background: #fff;
  cursor: pointer;
  font-weight: 600;
  color: var(--muted-ink);
  transition: border-color .18s var(--ease), background .18s var(--ease), color .18s var(--ease);
}
.dropzone:hover { border-color: var(--navy); color: var(--navy); }
.dropzone input[type="file"] { position: absolute; width: 1px; height: 1px; opacity: 0; }
.dropzone .dropzone-over { display: none; }
.dropzone.is-over { border-color: var(--coral-ink); background: #fff6f6; color: var(--coral-ink); }
.dropzone.is-over .dropzone-idle { display: none; }
.dropzone.is-over .dropzone-over { display: block; }
.dropzone-chosen { color: var(--ink); font-weight: 700; }

/* The « ? » that holds what used to be a paragraph. Inline with the line it explains, so it
   reads as an aside and not as a control somebody has to deal with. */
/* Inline WITH the line it explains — a « ? » alone on the next row reads as a control somebody
   forgot to label, which is the opposite of getting out of the way. */
.lede, .hint-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
.help { display: inline-block; vertical-align: baseline; position: relative; }
.help > summary {
  list-style: none; cursor: help; display: inline-grid; place-items: center;
  width: 17px; height: 17px; border-radius: 50%; border: 1px solid var(--line);
  font-size: 11px; font-weight: 800; color: var(--muted); background: #fff;
}
.help > summary::-webkit-details-marker { display: none; }
.help > summary:hover, .help[open] > summary { color: var(--ink); border-color: var(--ink); }
.help > p {
  position: absolute; z-index: 5; top: calc(100% + 6px); left: 0; width: min(360px, 78vw);
  margin: 0; padding: 10px 12px; font-size: 13px; font-weight: 400; line-height: 1.5;
  color: var(--ink); background: #fff; border: 1px solid var(--line);
  border-radius: var(--r); box-shadow: var(--shadow);
}
/* Refused before it is sent: the zone says so, and the publish button stays out of reach. */
.dropzone.is-toobig { border-color: var(--coral-ink); }
.dropzone-toobig { color: var(--coral-ink); font-weight: 700; }

.asset-grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
.asset {
  margin: 0; border: 1px solid var(--line); border-radius: var(--r);
  background: #fff; overflow: hidden; box-shadow: var(--shadow);
}
.asset img, .asset video, .asset-blank {
  display: block; width: 100%; height: 110px; object-fit: contain;
  background: repeating-conic-gradient(#f2f4f8 0% 25%, #fff 0% 50%) 50% / 16px 16px;
}
/* A video tile is black behind its own frame, like every player: the checkerboard is there to
   say "transparent", and a letterboxed video is not transparent. */
.asset video { background: #0d1220; }
/* Neither a picture nor a video — the extension, said plainly, instead of a broken image. */
.asset-file { display: grid; place-items: center; }
.asset-file span {
  font: 700 13px/1 var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  letter-spacing: .08em; color: var(--muted); border: 1px solid var(--line);
  border-radius: 6px; padding: 7px 10px; background: #fff;
}
.asset figcaption { padding: 9px 11px; display: grid; gap: 2px; font-size: 12.5px; word-break: break-all; }
.asset-target { display: grid; gap: 4px; }

/* The heading that splits a form into the questions it answers. Not an <h2>: the card already
   sits inside a section, and a second page-level heading would announce a second subject. */
.fields-group {
  /* The card is a two-column grid, so a heading that is just another grid item lands beside a
     field and titles nothing — measured: « CE QU'ON DEMANDE AU CANDIDAT » sat next to a switch
     that belonged to it. A full-row span is what makes it a heading rather than a cell. */
  grid-column: 1 / -1;
  margin: 4px 0 -6px; font-size: 12.5px; font-weight: 800; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted);
}
.fields-group:not(:first-child) { margin-top: 10px; }

/* A tile whose image errored keeps its height — a collapsed card next to eight normal ones reads
   as a layout glitch, not as a file the site did not serve. */
.asset.is-broken { border-color: var(--coral-ink); }
.asset.is-broken img, .asset.is-broken video { display: none; }
.asset.is-broken::before {
  content: ""; display: block; height: 110px; border-bottom: 1px solid var(--line);
  background: repeating-linear-gradient(45deg, #fff3f3 0 8px, #fff 8px 16px);
}
.asset-failed { color: var(--coral-ink); font-weight: 700; font-size: 12px; }

/* ═══ charts.css ═══ */
/* ── charts ──────────────────────────────────────────────────────────────── */

.chart-card { padding: 14px 16px 10px; }
.chart { width: 100%; height: 180px; display: block; }
.chart-grid { stroke: var(--line); stroke-width: 1; }
.chart-axis { font-size: 10px; fill: var(--muted-ink); font-family: inherit; }

.legend { list-style: none; display: flex; flex-wrap: wrap; gap: 14px; margin: 10px 0 0; padding: 0; font-size: 12.5px; color: var(--muted-ink); }
.legend li { display: flex; align-items: center; gap: 6px; }
.swatch { width: 10px; height: 10px; flex-shrink: 0; }

.kv-4 dd { font-size: 26px; }
.trend { margin: 6px 0 0; font-size: 12px; color: var(--muted-ink); }

/* ═══ maintenance.css ═══ */
/* ── maintenance ─────────────────────────────────────────────────────────── */

.maintenance { display: grid; gap: 10px; }
.maintenance.is-off { border-left: 4px solid var(--preparing); }
.maintenance p { margin: 0; }
.maintenance form { margin: 4px 0 0; }
.pill-maintenance { color: var(--preparing); background: #fdf5e6; }
.client-card.s-maintenance { border-left-color: var(--preparing); }
.pub-dot.pub-maintenance { background: var(--preparing); }

.is-hidden { display: none !important; }

/* ═══ onboarding.css ═══ */
/* ── L3: onboarding, checklist, deposit ──────────────────────────────────── */

.form-wide { max-width: 640px; }
.parc-head .right { margin-left: auto; }
.parc-head { gap: 10px; }
.form-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.field-error { display: block; margin-top: 4px; color: var(--error, #b42318); font-style: normal; font-size: 13px; }

/* ── declaring a client: three sections, one question at a time ───────────────
   `<details>`, so the folding needs no script and a closed section still submits the fields
   inside it. The server renders every one OPEN and the island folds the later ones away, which
   is what keeps the form whole with JavaScript off. The number comes from a counter rather than
   from the markup: a section inserted in the middle would otherwise renumber by hand. */
.onb { counter-reset: onbpart; max-width: 640px; }
.onb-part {
  counter-increment: onbpart;
  border: 1px solid var(--line, #dfe3ec);
  border-radius: 14px;
  background: #fff;
  padding: 0 16px;
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.onb-part[open] { border-color: #ccd3e6; box-shadow: var(--shadow); }
.onb-part > summary {
  display: grid;
  grid-template-columns: 26px 1fr;
  gap: 2px 12px;
  align-items: baseline;
  padding: 14px 0;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.onb-part > summary::-webkit-details-marker { display: none; }
.onb-part > summary::before {
  content: counter(onbpart);
  grid-row: 1 / span 3;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--paper);
  border: 1px solid var(--line, #dfe3ec);
  color: var(--muted-ink);
  font-size: 12.5px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}
.onb-part[open] > summary::before { background: var(--navy); border-color: var(--navy); color: #fff; }
/* That a section OPENS. The numbered disc says which step it is, and with the marker removed a
   folded section read as a card with nothing to press: the auto-reveal covers the way forward,
   and nothing said you could go back and check what you typed. */
.onb-part > summary::after {
  content: "";
  grid-column: 2;
  grid-row: 1;
  justify-self: end;
  align-self: center;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--muted-ink);
  border-bottom: 2px solid var(--muted-ink);
  transform: rotate(45deg);
  transition: transform .18s var(--ease);
}
.onb-part[open] > summary::after { transform: rotate(-135deg); }
.onb-part-title { font-size: 16px; letter-spacing: -.01em; }
.onb-part-what { font-size: 13px; font-weight: 400; color: var(--muted-ink); line-height: 1.4; }
/* What the section already holds, for a reader who has folded it. Shown only when folded: open,
   the fields themselves say it, and repeating them above is a second copy going stale. */
.onb-part > label,
.onb-part > .notice { margin-bottom: 14px; }
.onb-part > label:last-child { margin-bottom: 18px; }
.onb-part > label + label { display: block; }

/* A hint under a field is a hint: `.form label` is bold by design, and inheriting that turns
   every explanation into a second label competing with the real one. */
.form label small { display: block; margin-top: 4px; font-weight: 400; font-size: 12.5px; color: var(--muted-ink); }
.form textarea {
  width: 100%; font: inherit; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12.5px; padding: 10px 12px; border: 1px solid var(--line, #dfe3ec); border-radius: 10px;
  background: #fff; color: var(--ink); resize: vertical;
}
.form textarea:focus { outline: 2px solid var(--accent, #202e5e); outline-offset: 1px; }

.steps { list-style: none; padding: 0; margin: 0 0 18px; display: grid; gap: 8px; }
.step { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap;
        border-left: 3px solid var(--line, #dfe3ec); padding: 6px 12px; }
.step code { font-size: 12px; color: var(--muted-ink); }
.step-state { font-size: 13px; color: var(--muted-ink); }
.step-done { border-left-color: var(--online, #0f7a3d); }
.step-already { border-left-color: var(--line, #dfe3ec); }
.step-failed { border-left-color: var(--error, #b42318); }
.step-skipped { opacity: .6; }

/* A bar in ten steps rather than an inline width — the CSP forbids style="", and a progress
   bar is not a good reason to weaken it. */
.progress { position: relative; height: 26px; border-radius: 999px; background: #eef0f6; overflow: hidden; margin: 0 0 20px; }
.progress-bar { height: 100%; background: var(--online, #0f7a3d); opacity: .22; transition: width .3s ease; }
.progress-0 { width: 0; } .progress-10 { width: 10%; } .progress-20 { width: 20%; }
.progress-30 { width: 30%; } .progress-40 { width: 40%; } .progress-50 { width: 50%; }
.progress-60 { width: 60%; } .progress-70 { width: 70%; } .progress-80 { width: 80%; }
.progress-90 { width: 90%; } .progress-100 { width: 100%; }
.progress-label { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
                  font-size: 13px; font-weight: 700; }

.checklist { list-style: none; counter-reset: step; padding: 0; margin: 0 0 12px; display: grid; gap: 0; overflow: hidden; }
/* One row per step in one card: nine cards each with a dot, a pill and a « ? » read as nine
   alarms; nine rows read as a list somebody works down. */
.check-step { padding: 12px 16px; background: #fff; border-top: 1px solid var(--line, #dfe3ec); }
.check-step:first-child { border-top: 0; }
.check-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* The state, readable from across the room: a mark with its own glyph AND its own colour, then
   the pill saying it in words. A ten-pixel dot asked the reader to compare shades. */
.check-dot {
  width: 26px; height: 26px; border-radius: 50%; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 800; line-height: 1;
  background: #f1f3f7; color: var(--unknown, #5b6577);
}
.check-dot::before { content: "?"; }
.is-done .check-dot { background: #eef8f2; color: var(--online, #0f7a3d); }
.is-done .check-dot::before { content: "\2713"; }
.is-todo .check-dot { background: #fdf5e6; color: var(--preparing, #b98900); }
.is-todo .check-dot::before { content: "!"; }
.is-blocked .check-dot { background: #fdeceb; color: var(--error, #b42318); }
.is-blocked .check-dot::before { content: "\2715"; }
.is-loading .check-dot { background: #f1f3f7; color: var(--muted-ink); }
.is-loading .check-dot::before { content: "\2026"; }
.check-head > b { font-size: 15px; }
.check-head .pill { font-size: 13px; padding: 4px 11px; }
.pill-loading { color: var(--muted-ink); background: #f1f3f7; }
.pill-loading::before { content: none; }
.pill-loading .spinner { width: 12px; height: 12px; border-width: 2px; border-color: rgba(89, 96, 113, .3); border-top-color: var(--muted-ink); display: inline-block; }
.pill-done { color: var(--online, #0f7a3d); background: #eef8f2; }
.pill-todo { color: var(--preparing, #b98900); background: #fdf5e6; }
.check-value code { font-size: 12.5px; }
.check-command { margin: 8px 0 0; padding: 8px 10px; background: #0f1424; color: #e7ebf5;
                 border-radius: 8px; overflow-x: auto; font-size: 12.5px; }

.kit-card { margin-bottom: 20px; }
.kit-card h2 { margin-top: 0; }
.verdict { display: block; padding: 14px 16px; }
.verdict-ok { border-left: 4px solid var(--online, #0f7a3d); }
.verdict-ko { border-left: 4px solid var(--error, #b42318); }
.file-list { list-style: none; padding: 0; margin: 0 0 12px; display: grid; gap: 4px; font-size: 13px; }
.file-list small { color: var(--muted-ink); }
.fix { display: block; font-size: 13px; color: var(--muted-ink); margin-top: 2px; }
.findings .finding b { display: block; font-size: 14.5px; }

/* ── the deposit screen: four cards, two by two ────────────────────────────── */
.page-grid { display: grid; gap: 16px; margin-bottom: 20px; }
@media (min-width: 900px) { .page-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.page-grid > .card { margin: 0; display: flex; flex-direction: column; }
.page-grid > .card > p:last-child, .page-grid > .card > .hint:last-child { margin-bottom: 0; }
.page-grid .kit-card > p:nth-child(3) { margin-top: auto; }
.deposit-card .form { max-width: none; }
.deposit-card .dropzone { margin-top: 0; }
.deposit-card .form-actions { margin-top: 12px; flex-wrap: wrap; }

/* The two action cards above the list are cards, not one block: a rule of air between them. */
.card.resume { margin-bottom: 12px; }

/* ═══ quality.css ═══ */
/* ── L5: what was delivered ──────────────────────────────────────────────── */

/* Two stacked sections, and the pass that may refuse is separated from the one that may only
   observe by WORDS first — the sentence under each title, not a colour an operator has to learn. */
.quality-pass { margin: 0 0 26px; }
.quality-head { margin: 0 0 10px; }
.quality-head h2 { margin: 0; }
.authority { margin: 4px 0 0; font-size: 13px; color: var(--muted-ink); max-width: 62ch; }
.quality-card { padding: 16px 18px; }
.quality-verdict { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.quality-subject { font-size: 13px; color: var(--muted-ink); word-break: break-word; }
.quality-truncated { margin: 10px 0 0; font-size: 12.5px; }
.quality-findings { margin-top: 14px; }

/* A verdict pill per pass. `observed` is deliberately neutral: the live pass has nothing to
   approve, and a green tick there would read as an endorsement it has no authority to give. */
.pill-q-pass { color: var(--online, #0f7a3d); background: #eef8f2; }
.pill-q-warnings { color: var(--preparing, #b98900); background: #fdf5e6; }
.pill-q-refused { color: var(--error, #b42318); background: #fdeceb; }
.pill-q-observed { color: var(--muted-ink); background: #f1f3f7; }
/* `skipped` wears the colour of an unknown check row, not a neutral grey: nothing was measured,
   and the pill must not be mistaken at a glance for a pass that had nothing to say. */
.pill-q-skipped { color: var(--muted-ink); background: #f1f3f7; border: 1px dashed #cfd4e0; }

.check-rows { list-style: none; padding: 0; margin: 14px 0 0; display: grid; gap: 2px; }
.check-row {
  display: grid; grid-template-columns: 10px 1fr auto; align-items: center; gap: 10px;
  padding: 7px 0; border-bottom: 1px solid var(--line, #dfe3ec); font-size: 14px;
}
.check-row:last-child { border-bottom: 0; }
.q-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--unknown, #8a90a2); }
.q-ok .q-dot { background: var(--online, #0f7a3d); }
.q-warn .q-dot { background: var(--preparing, #b98900); }
.q-fail .q-dot { background: var(--error, #b42318); }
.q-unknown .q-dot { background: #cfd4e0; }
.q-observed { color: var(--muted-ink); font-size: 13px; text-align: right; }
/* An unmeasured check is dimmer than a measured one, but still legible: "we did not look" is
   information, not noise. */
.q-unknown .q-label { color: var(--muted-ink); }

/* The skeleton has the SHAPE of the panel: a subject line, then a row per check. */
.sk-md { width: 40%; height: 12px; }
.sk-dot { width: 9px; height: 9px; border-radius: 50%; }
[data-quality-panel] { transition: opacity .18s ease; }
[data-quality-panel].is-loading { opacity: .45; }

/* ═══ previews.css ═══ */
/* ── L6: the previews ────────────────────────────────────────────────────────
   Five states with five names, because a preview that is being built, one that is live, one
   that has lapsed and one nobody can date are four different things to do next. `undated` is
   deliberately the same amber as a warning: it is not an error, it is a thing to look at. */
.preview-card { margin: 0 0 22px; }
.preview-state { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 0 0 10px; }
.preview-url { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; word-break: break-all; }
.preview-meta { display: flex; gap: 14px; flex-wrap: wrap; font-size: 13px; color: var(--muted-ink); margin-top: 6px; }

.pill-preview-live { color: var(--online, #0f7a3d); background: #eef8f2; }
.pill-preview-building { color: var(--preparing, #b98900); background: #fdf5e6; }
.pill-preview-expired { color: var(--muted-ink); background: #f1f3f7; }
.pill-preview-undated { color: var(--preparing, #b98900); background: #fdf5e6; }
.pill-kind-client { color: var(--muted-ink); background: #f1f3f7; }
.pill-kind-demo { color: #464396; background: #eeeefb; }

.preview-list { list-style: none; padding: 0; margin: 18px 0 0; display: grid; gap: 12px; }
.preview-row {
  padding: 14px 16px; border: 1px solid var(--line, #dfe3ec); border-radius: 10px;
  background: var(--card, #fff); transition: border-color .18s ease, transform .18s ease;
}
.preview-row:hover { border-color: #c7cddb; }
.preview-row-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; }
/* An expired preview is dimmed but never hidden: it is still a branch somebody has to remove,
   and a list that quietly drops them is a list that stops explaining the disk filling up. */
.preview-expired { opacity: .72; }
.btn-small { padding: 3px 10px; font-size: 12.5px; }
.btn.is-copied { color: var(--online, #0f7a3d); }

/* ═══ settings.css ═══ */
/* ── L7: the parc's shared values ────────────────────────────────────────────
   Two tables, one visual language, and no second one invented for them: the pills, the
   skeleton and the card are the primitives every other screen already uses. What is specific
   here is the line under each name — where the value lands and who reads it — because that is
   the thing an operator needs beside the field, not in a paragraph at the top. */
.globals td { vertical-align: top; }
.globals .g-name { display: block; font-weight: 600; }
.g-lands { display: block; font-size: 12px; color: var(--muted-ink); margin-top: 3px; }
.g-value { margin: 0; word-break: break-all; }
/* A refusal is amber, never red: nothing is broken — we are declining to write it, and the
   sentence next to it says why. */
.g-refused { margin: 4px 0 0; font-size: 12.5px; color: var(--preparing, #b98900); max-width: 46ch; }
.g-form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 0; }
.g-form input[name="value"] { min-width: 15rem; flex: 1 1 15rem; }
.g-fleet { margin-top: 18px; }

/* A row rests until somebody asks for it. The fold is a `<details>`, so it opens with no script
   and the form inside still submits with JavaScript off; the summary is a quiet button because
   thirty filled ones down a table read as thirty things to do. */
details.g-edit { margin-top: 8px; }
details.g-edit > summary { list-style: none; user-select: none; }
details.g-edit > summary::-webkit-details-marker { display: none; }
details.g-edit[open] > summary { background: var(--paper); }
details.g-edit > .g-form { margin-top: 10px; }
/* The open row is the one being changed: a left rule says which, without moving anything. */
details.g-edit[open] { border-left: 3px solid var(--navy); padding-left: 10px; margin-left: -13px; }
.g-remove { margin-top: 8px; }

/* ── the prompt a write leaves behind ────────────────────────────────────────
   The value is written and nothing has moved yet; this is the second half of the gesture, not a
   modal. It sits under the intro, carries the name that changed, and disappears with its flash. */
.deploy-prompt { border-left: 3px solid var(--preparing, #b98900); margin-bottom: 20px; }
/* A heading, not a shout: the section titles of this screen are uppercase and this one is a
   sentence about a name somebody just typed. */
.deploy-prompt-title { margin: 0 0 6px; font-size: 17px; text-transform: none; letter-spacing: 0; }
.deploy-prompt .hint { margin: 0 0 14px; max-width: 74ch; }
.deploy-pick-set { border: 0; margin: 0; padding: 0; }
.deploy-pick-set legend { padding: 0; font-size: 13px; font-weight: 700; color: var(--muted-ink); }
.deploy-pick-all, .deploy-pick-one {
  display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap;
  padding: 7px 0; border-bottom: 1px solid var(--line, #dfe3ec); cursor: pointer;
}
.deploy-pick-all { font-weight: 700; }
.deploy-pick-one:last-child { border-bottom: 0; }
.deploy-pick-one .mono { font-size: 12.5px; }
.deploy-prompt-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 14px; }

.prop-card { margin: 0; }
.prop-count { display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap; margin: 0 0 12px; }
.prop-rows { list-style: none; margin: 0; padding: 0; }
.prop-row {
  display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
  padding: 9px 0; border-bottom: 1px solid var(--line, #dfe3ec);
}
.prop-row:last-child { border-bottom: 0; }
.prop-client { font-weight: 600; }
.prop-detail { font-size: 13px; color: var(--muted-ink); flex: 1 1 18rem; }

/* Eight states, and the colours say the same thing the words do: green only for a value that
   actually arrived, amber for everything still to happen, grey for what this rotation does not
   concern, red only for a deployment that failed. `unknown` is grey and says so — it is the
   colour of "we did not observe this", never of "fine". */
.pill-prop-received { color: var(--online, #0f7a3d); background: #eef8f2; }
.pill-prop-waiting { color: var(--preparing, #b98900); background: #fdf5e6; }
.pill-prop-in_flight { color: var(--preparing, #b98900); background: #fdf5e6; }
.pill-prop-nothing_shipped { color: var(--preparing, #b98900); background: #fdf5e6; }
.pill-prop-failed { color: var(--error, #b42318); background: #fdeceb; }
.pill-prop-not_deployable { color: var(--blocked, #6b4ea8); background: #f3eefe; }
.pill-prop-overridden { color: var(--muted-ink); background: #f1f3f7; }
.pill-prop-unknown { color: var(--muted-ink); background: #f1f3f7; }

[data-propagation-panel].is-loading { opacity: .45; }

/* Three columns do not fit a phone: each row becomes a small block, name on top, value under it,
   the date last. The header row is the column names, which the blocks no longer need. */
@media (max-width: 720px) {
  .globals thead { display: none; }
  .globals tr { display: block; padding: 10px 0; border-bottom: 1px solid var(--line); }
  .globals tr:last-child { border-bottom: 0; }
  .globals td { display: block; padding: 2px 14px; border: 0; }
  .globals td.nowrap { white-space: normal; font-size: 12px; color: var(--muted-ink); }
  .g-form input[name="value"] { min-width: 0; }
  .g-form button { flex: 1 1 auto; }
  .prop-detail { flex-basis: 100%; }
  /* On a phone the fold is already indented by the table cell; the rule alone is enough. */
  details.g-edit[open] { margin-left: 0; padding-left: 8px; }
  .deploy-prompt-actions .btn { flex: 1 1 auto; }
}

/* ═══ alerts.css ═══ */
/* ── alertes (L4) ─────────────────────────────────────────────────────────── */

/* The badge rides ON the nav link (see layout.php): a badge that is its own element wraps onto
   its own line the first time the window is narrow, and then it is a number belonging to
   nothing. */
.navbadge {
  display: inline-block;
  min-width: 18px;
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--error);
  color: #fff;
  font-size: 11px;
  font-weight: 800;
  line-height: 16px;
  text-align: center;
  vertical-align: 1px;
}

/* An alert nobody has taken carries the accent on its edge; a taken one keeps its place in the
   list and stops shouting. The difference is deliberately a WEIGHT and not a colour swap: two
   states that differ only in hue are two states somebody stops telling apart at a glance. */
.alert { border-left: 3px solid var(--line); }
.alert-new { border-left-color: var(--error); }
.alert-taken { border-left-color: var(--line); opacity: .82; }
.alert header { display: flex; align-items: center; gap: 10px; justify-content: space-between; }
.alert header h2 { margin: 0; font-size: 15px; text-transform: none; letter-spacing: 0; color: var(--ink); }
.alert-what { margin: 10px 0 0; font-size: 14px; font-weight: 600; }
.alert-fix { margin: 6px 0 0; font-size: 13px; color: var(--muted-ink); }
.alert .meta { margin: 10px 0 0; font-size: 12px; color: var(--muted-ink); }
.alert-notify-error {
  margin: 10px 0 0;
  padding: 8px 10px;
  border-radius: var(--r-sm);
  background: #fdeceb;
  color: var(--error);
  font-size: 12px;
}
.alert form { margin-top: 12px; }

.pill-blocker { color: var(--error); background: #fdeceb; }
.pill-warning { color: var(--preparing); background: #fdf5e6; }

/* Where the alerts go. Folded once they go somewhere: it is maintenance then, not a decision. */
.setup { margin-bottom: 18px; }
.setup > summary {
  cursor: pointer;
  font-size: 13px;
  font-weight: 700;
  color: var(--muted-ink);
  padding: 6px 0;
}
.setup > summary:hover { color: var(--navy); }
.setup .form { margin-top: 10px; max-width: 560px; }

h2.section { margin-top: 32px; }

/* Where the alerts go: a fold like the client screen's, and a card once it is open, so a form
   with three fields does not float on the page background beside a native button. */
details.setup { margin: 0 0 20px; }
details.setup > summary { cursor: pointer; font-size: 13px; font-weight: 700; color: var(--muted-ink); padding: 8px 0; list-style: none; }
details.setup > summary::-webkit-details-marker { display: none; }
details.setup > summary::before { content: "▸ "; }
details.setup[open] > summary::before { content: "▾ "; }
details.setup > .form { margin-top: 6px; padding: 16px; background: var(--card); border: 1px solid var(--line); border-radius: var(--r); box-shadow: var(--shadow); max-width: 560px; }
details.setup > .form .btn { margin-top: 4px; }

/* ═══ disclosure.css ═══ */
/* ── progressive disclosure ───────────────────────────────────────────────────
 * Three patterns, one idea: what the reader has to act on stays on screen, what
 * answers "how did we get here" is one click away and says so.
 */

/* A folded group of findings: the gesture once, every name under it. Names are chips
   rather than rows because they are read as a set — "these fourteen" — not one by one. */
.finding.is-group .finding-names {
  list-style: none;
  margin: 10px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.finding.is-group .finding-names li {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px 6px;
  min-width: 0;
  background: rgba(255, 255, 255, .75);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 3px 10px;
  max-width: 100%;
}
/* `break-all` hyphenated PHOTO_SERVICE_URL in the middle of a word while there was a space
   right after it. `anywhere` breaks at the space first and only splits a word that genuinely
   cannot fit. */
.finding.is-group .finding-names .mono { font-weight: 700; color: var(--ink); overflow-wrap: anywhere; }
.finding.is-group .finding-names small { color: var(--muted-ink); font-size: 11.5px; }

/* One fact, and the way to the rest of them. */
.card.oneline { display: flex; align-items: baseline; gap: 10px 14px; flex-wrap: wrap; }
.card.oneline .oneline-more { margin-left: auto; font-size: 13.5px; font-weight: 700; white-space: nowrap; }
.card.oneline .hint { flex-basis: 100%; }

/* A fold whose content changes the site for visitors. Named, not shouted: the red is on the
   button and on the confirmation, which is where the decision actually happens. */
details.expert-danger > summary { color: var(--error); }
details.expert-danger[open] { border-left: 3px solid var(--error); padding-left: 12px; }

/* A folded section that hides something the reader must see — a value their form rejected. */
.dot-alert {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--error);
  vertical-align: middle;
}

/* ═══ menus.css ═══ */
/* ── menus (details/summary, no script) ───────────────────────────────────────
 * A menu that needs JS is a menu that is missing on the first paint, and this one carries
 * signing out.
 */
.menu { position: relative; }
.menu > summary { cursor: pointer; list-style: none; user-select: none; }
.menu > summary::-webkit-details-marker { display: none; }
.menu[open] > summary { position: relative; z-index: 2; }
.menu-panel {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  z-index: 3;
  min-width: 190px;
  display: grid;
  gap: 2px;
  padding: 6px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--shadow-lift);
  animation: rise .18s var(--ease) both;
}
.menu-panel a,
.menu-panel button {
  display: block;
  width: 100%;
  text-align: left;
  padding: 9px 12px;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
  cursor: pointer;
}
.menu-panel a:hover, .menu-panel button:hover { background: var(--paper); color: var(--navy); }
.menu-panel .menu-who {
  padding: 4px 12px 8px;
  font-size: 12px;
  color: var(--muted-ink);
  border-bottom: 1px solid var(--line);
  margin-bottom: 4px;
}
.menu-panel .inline-form { margin: 0; }

/* In the top bar the trigger is a nav item like any other. */
.nav-menu > summary {
  padding: 7px 11px;
  border-radius: 999px;
  font-size: 14px;
  font-weight: 700;
  color: rgba(255, 255, 255, .88);
  display: flex;
  align-items: center;
  gap: 5px;
}
.nav-menu > summary:hover { background: rgba(255, 255, 255, .14); color: #fff; }
.nav-menu[open] > summary { background: rgba(255, 255, 255, .2); color: #fff; }
.nav-menu .caret { font-size: 10px; opacity: .8; }
.whoami-name { max-width: 12ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ═══ tabs-bar.css ═══ */
/* A tab bar that wraps onto three rows on a phone is a wall; one that scrolls is a bar.
   Scoped to `.tabs-bar`: the sub-tab rows (`.tabs-sub`, in edit and stats) have no track to
   scroll, so taking `flex-wrap` away from them would push the page itself sideways. */
.tabs-bar { flex-wrap: nowrap; align-items: stretch; }
.tabs-track {
  display: flex;
  gap: 2px;
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.tabs-track::-webkit-scrollbar { display: none; }
.tabs-track .tab { white-space: nowrap; flex: 0 0 auto; }
/* At 390px four of the eight tabs are off-screen and the only hint was a half-cut word. Two
   gradients that scroll WITH the content (`local`) sit over two that do not (`scroll`): the cover
   hides the shadow at each end, so a bar with nothing to scroll shows nothing, and a bar with
   more to the right shows that there is more to the right. */
.tabs-track {
  background:
    linear-gradient(to right, var(--paper) 60%, rgba(247, 248, 251, 0)) left center / 22px 100% no-repeat local,
    linear-gradient(to left, var(--paper) 60%, rgba(247, 248, 251, 0)) right center / 22px 100% no-repeat local,
    linear-gradient(to right, rgba(26, 31, 46, .22), rgba(26, 31, 46, 0)) left center / 18px 100% no-repeat scroll,
    linear-gradient(to left, rgba(26, 31, 46, .22), rgba(26, 31, 46, 0)) right center / 18px 100% no-repeat scroll;
}

/* ═══ history.css ═══ */
/* ── the chronology (history) ─────────────────────────────────────────────
   One list, not two tables. A row is a stack on a phone (when, what, act) and three columns
   from 720px up, so the dates line up into a rail the eye can run down. The left rule says
   where the change came from without needing a word for it: coral for the manager, plain line
   for a hand edit on the repository. */
.chrono { list-style: none; margin: 0; padding: 0; }
.chrono-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 4px 16px;
  padding: 14px 16px;
  background: var(--card);
  border: 1px solid var(--line);
  border-left: 3px solid var(--line);
  border-radius: var(--r-sm);
  margin-bottom: 8px;
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.chrono-row:last-child { margin-bottom: 0; }
.chrono-row.is-manager { border-left-color: var(--coral); }
.chrono-row.is-removed { border-left-color: var(--unknown); background: var(--paper); }
.chrono-row:hover { box-shadow: var(--shadow); }
.chrono-when { color: var(--muted-ink); font-size: 13px; white-space: nowrap; font-variant-numeric: tabular-nums; }
.chrono-title { margin: 0; font-weight: 600; font-size: 15px; line-height: 1.35; }
.chrono-title .chip-static { vertical-align: middle; margin-left: 6px; }
.chrono-meta {
  margin: 3px 0 0;
  font-size: 13px;
  color: var(--muted-ink);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 6px;
}
.chrono-origin { font-weight: 600; }
.chrono-row.is-manager .chrono-origin { color: var(--coral-ink); }
.chrono-sep { opacity: .55; }
.chrono-act { display: flex; align-items: flex-start; }
.chrono-act:empty { display: none; }
.chip-warn { border-color: var(--preparing); color: var(--preparing); }
.sk-btn { width: 132px; height: 30px; border-radius: var(--r-sm); }

@media (min-width: 720px) {
  .chrono-row {
    grid-template-columns: 132px minmax(0, 1fr) auto;
    align-items: start;
    padding: 16px 18px;
  }
  .chrono-when { padding-top: 2px; }
  .chrono-act { justify-content: flex-end; }
  .chrono-act:empty { display: block; }
}

/* ═══ hosting.css ═══ */
/* ── B42: the hosting mode, the domain check, the handover ───────────────────
   Three small blocks, all on screens that already exist. Nothing here introduces a new visual
   idea: the mode is a pair of cards like the rest of the form, the check is an inline answer
   next to the field it is about, and the handover is a card with a monospace block the operator
   copies. */

.modes { display: grid; gap: 10px; border: 0; padding: 0; margin: 0 0 16px; }
@media (min-width: 720px) { .modes { grid-template-columns: 1fr 1fr; } }
.mode {
  display: flex; gap: 10px; align-items: flex-start;
  border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px;
  background: #fff; cursor: pointer;
  transition: border-color .15s ease, background .15s ease;
}
.mode:hover { border-color: var(--navy); }
/* The selected card carries the state, not just the radio: a dot 14 px wide is not a state you
   can read across a form. */
.mode:has(input:checked) { border-color: var(--navy); background: #f7f9ff; }
 /* `.form label input` is 100 % wide by design, which turns a radio into a full-width control
    centred over its own label. A radio is 14 px and stays 14 px. */
.mode input { width: auto; flex: 0 0 auto; margin-top: 3px; }
.mode b { display: block; font-size: 14px; }
.mode small { display: block; margin-top: 3px; font-size: 12.5px; color: var(--muted-ink); font-weight: 400; }

.domain-check { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 8px 0 0; }
.domain-check output { font-size: 13px; font-weight: 600; color: var(--muted-ink); }
.domain-check output.is-available { color: var(--online); }
.domain-check output.is-ours { color: var(--online); }
.domain-check output.is-unverified,
.domain-check output.is-foreign { color: var(--blocked); }
.domain-check output.is-unknown,
.domain-check output.is-unconfigured { color: var(--muted-ink); }

.notice-blocked { border-color: #ddd0fb; background: #f7f3ff; }
.notice-blocked b { display: block; margin-bottom: 4px; }
.notice-blocked p { margin: 6px 0 0; }

.handover {
  border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px;
  background: #fff; margin: 0 0 18px;
}
.handover h2 { margin: 0 0 6px; }
.handover-blocked { border-left: 3px solid var(--blocked); }
.handover-facts { display: grid; gap: 8px; margin: 12px 0; }
@media (min-width: 720px) { .handover-facts { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.handover-facts dt { font-size: 12px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted-ink); }
.handover-facts dd { margin: 3px 0 0; }
.handover-facts code { font-size: 13px; word-break: break-all; }
.handover-message {
  white-space: pre-wrap; word-break: break-word;
  background: #f7f8fb; border: 1px solid var(--line); border-radius: 10px;
  padding: 12px 14px; font-size: 12.5px; color: var(--ink); margin: 0 0 10px;
}

/* Accepted and not finished: neither the green of a step that ran nor the red of one that
   broke, because the answer to it is « resubmit », not « find out what went wrong ». */
.step-pending { border-left-color: var(--blocked); }

/* Under the row it is about: indented past the mark, quieter than a card of its own. */
.check-step .handover-inline { margin: 10px 0 4px 36px; padding: 12px 14px; }
.handover-inline h3 { margin: 0 0 4px; font-size: 14px; }
.handover-inline .why { margin: 0; font-size: 13px; color: var(--muted-ink); }
.handover-inline .handover-facts { margin: 10px 0; }

/* ═══ logs.css ═══ */
/* ── the API journal: what happened between this client and the outside ─────
   A list somebody reads while chasing one failure, so a row leads with the three things that
   place it (direction, service, status) and the address underneath in full. Not a table: six
   columns on a phone is a horizontal scrollbar over the exact screen a chargé de compte opens
   standing in a client's office. */
.log-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.log-row { padding: 13px 16px; transition: box-shadow .18s var(--ease), border-color .18s var(--ease); }
.log-row:hover { box-shadow: var(--shadow-lift); border-color: #ccd3e6; }
/* A failed exchange is marked on the EDGE, not by tinting the card: the reader is scanning a
   column of forty rows for the red one, and a full tint at that density reads as an alarm about
   the screen rather than about one line. */
.log-row.is-failed { border-left: 3px solid var(--error); }
.log-head { display: flex; align-items: center; gap: 8px 10px; flex-wrap: wrap; }
.log-head > .btn { flex: 0 0 auto; margin-left: auto; }
/* Direction is the first thing read and the cheapest to get wrong: « on nous a envoyé » and
   « on a envoyé » are opposite diagnoses, so it gets a badge of its own rather than a word in
   the meta line. */
.log-dir {
  flex: 0 0 auto;
  font-size: 11px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .05em;
  padding: 3px 8px;
  border-radius: 999px;
}
.log-dir-in { color: var(--blocked); background: #f3eefe; }
.log-dir-out { color: var(--navy); background: #eef1fa; }
/* A chip that names something instead of filtering by it: same shape, none of the affordance. */
.chip-static { cursor: default; }
.chip-static:hover { border-color: var(--line); color: var(--muted-ink); }
.pill-log-ok { color: var(--online); background: #eef8f2; }
.pill-log-fail { color: var(--error); background: #fdeceb; }
/* Urls in this journal are the RAW request line, doubled slashes and all — that is what the
   thing is for — so they must wrap rather than be trimmed to something tidy and wrong. */
.log-url { margin: 8px 0 0; word-break: break-all; line-height: 1.45; }
.log-meta { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; margin: 6px 0 0; font-size: 12.5px; color: var(--muted-ink); }
.log-meta small { font-size: inherit; }
.log-sep { color: var(--muted); }
.log-error { margin: 8px 0 0; font-size: 13px; font-weight: 600; color: var(--error); }
.kv dd.is-bad { color: var(--error); }
.log-filters { margin-bottom: 10px; }
.log-filters .log-dir { padding: 2px 6px; font-size: 10px; }
.log-search { display: flex; gap: 8px; align-items: center; margin: 0 0 16px; }
.log-search input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 9px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font: inherit;
  font-size: 14px;
  color: var(--ink);
  background: var(--card);
}
.log-search input:focus { outline: 2px solid var(--navy); outline-offset: 1px; border-color: var(--navy); }
.logs-source { margin-top: 18px; font-size: 12px; }
/* One exchange, whole. */
.log-entry { padding: 16px; }
.log-headers { font-size: 12.5px; }
.log-headers th { text-align: left; font-weight: 700; white-space: nowrap; vertical-align: top; }
.log-headers td { word-break: break-all; }
/* A body is text from somebody else's server: it wraps, it scrolls if it is enormous, and it is
   never allowed to widen the page — an element wider than the viewport detaches everything
   positioned against the edge of it. */
.log-body {
  margin: 10px 0 0;
  padding: 12px 14px;
  max-height: 60vh;
  overflow: auto;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  white-space: pre-wrap;
  word-break: break-word;
  line-height: 1.5;
}
/* Same as the other panels: a filter change dims what is there instead of blanking it back to a
   skeleton, which would flash on a fast client and read as a glitch rather than as speed. */
[data-logs-panel] { transition: opacity .18s ease; scroll-margin-top: 104px; }

/* ═══ wire.css ═══ */
/* ── the wiring, watched (B25) ────────────────────────────────────────────────
   A modal only for the minute the wiring lasts. It is a `<dialog>`, so the browser gives it the
   focus trap, the Escape key and the backdrop; what is added here is the one thing the list has
   to do well, which is to say at a glance which step is being attempted right now. */
.wire {
  width: min(560px, calc(100vw - 24px));
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 22px;
  background: var(--card);
  color: var(--ink);
  box-shadow: var(--shadow-lift);
  max-height: calc(100dvh - 32px);
  overflow: auto;
}
.wire::backdrop { background: rgba(22, 32, 74, .38); }
.wire-title { margin: 0 0 6px; font-size: 18px; }
.wire-lede { margin: 0 0 16px; color: var(--muted-ink); font-size: 14px; min-height: 20px; }

.wire-steps { list-style: none; padding: 0; margin: 0; display: grid; gap: 2px; }
/* A grid and not a wrapping flex row: flex wraps a whole item before it shrinks one, so a step
   whose name is two words too long sends « fait » onto a line of its own and the list stops
   reading as a column of states. Here the name has the only flexible track and wraps inside it. */
.wire-step {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 4px 10px;
  align-items: baseline;
  padding: 8px 10px; border-radius: var(--r-sm);
  transition: background .18s var(--ease), opacity .18s var(--ease);
}
/* The name takes what is left and wraps INSIDE itself; the state never gets pushed onto a line
   of its own, which on a phone is what turns a seven line list into eleven. */
.wire-step b { font-weight: 600; font-size: 14px; }
.wire-state { font-size: 13px; color: var(--muted-ink); justify-self: end; }
/* Under the name it belongs to, never under the dot: it is a value, not a step. */
.wire-detail { grid-column: 2 / -1; font-size: 12px; color: var(--muted-ink); word-break: break-word; }

/* The dot is the whole progress indicator: a ring while a step waits, the state's colour once it
   has answered, and a pulse on the one being attempted. */
.wire-dot {
  width: 9px; height: 9px; border-radius: 50%;
  border: 2px solid var(--line); background: transparent;
  align-self: center;
}
.wire-step[data-state="todo"] { opacity: .55; }
.wire-step[data-state="running"] { background: var(--paper); }
.wire-step[data-state="running"] .wire-dot { border-color: var(--navy); background: var(--navy); animation: wire-pulse 1.1s ease-in-out infinite; }
.wire-step[data-state="done"] .wire-dot { border-color: var(--online); background: var(--online); }
.wire-step[data-state="already"] .wire-dot { border-color: var(--unknown); background: var(--unknown); }
.wire-step[data-state="pending"] .wire-dot { border-color: var(--preparing); background: var(--preparing); }
.wire-step[data-state="failed"] .wire-dot { border-color: var(--error); background: var(--error); }
.wire-step[data-state="failed"] .wire-state { color: var(--error); }
.wire-step[data-state="skipped"] { opacity: .5; }

@keyframes wire-pulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.45); opacity: .55; }
}
@media (prefers-reduced-motion: reduce) {
  .wire-step[data-state="running"] .wire-dot { animation: none; }
}

.wire-note { margin: 16px 0 0; font-size: 13px; color: var(--muted-ink); }
.wire-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; }
/* `.btn` sets `display: inline-flex`, which has the same specificity as the browser's own rule
   for `[hidden]` and comes after it: without this, the close button of a wiring still in flight
   is on screen, offering to walk away from a sequence that is halfway through. */
.wire [hidden] { display: none; }

/* ═══ overview.css ═══ */
/* ── the overview: what to do, how it is doing, what to press ──────────────── */

.overview { display: grid; gap: 20px; align-items: start; }
@media (min-width: 1000px) { .overview { grid-template-columns: minmax(0, 1fr) 320px; gap: 24px; } }
.overview-main { display: grid; gap: 16px; min-width: 0; }
.overview-side { display: grid; gap: 16px; }

/* A card whose rows are a list: no padding of its own, a head line, rows separated by rules. */
.list-card { padding: 0; overflow: hidden; }
.card-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 12px 16px; }
.card-head h2 { margin: 0; font-size: 12px; letter-spacing: .08em; }
.card-empty { margin: 0; padding: 0 16px 14px; color: var(--muted-ink); }
.card-foot { margin: 0; padding: 10px 16px 14px; border-top: 1px solid var(--line); }
.count { font-size: 12px; font-weight: 700; border-radius: 999px; padding: 1px 8px; font-variant-numeric: tabular-nums; }
.count-error { color: var(--error); background: #fdeceb; }
.count-warn { color: var(--preparing); background: #fdf5e6; }

/* The verdict sits in the health card as its first row, not as a box of its own. */
.list-card .verdict { border: 0; border-top: 1px solid var(--line); border-radius: 0; padding: 12px 16px; }

.kpis { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border-top: 1px solid var(--line); }
@media (min-width: 720px) { .kpis { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.kpis-1, .kpis.kpis-1 { grid-template-columns: minmax(0, 1fr); }
.kpi { display: flex; flex-direction: column; gap: 3px; padding: 14px 16px; min-width: 0; }
.kpi + .kpi { border-left: 1px solid var(--line); }
@media (max-width: 719px) { .kpi:nth-child(3) { border-left: 0; } .kpi:nth-child(n + 3) { border-top: 1px solid var(--line); } }
.kpi b { font-size: 24px; font-weight: 800; letter-spacing: -.02em; line-height: 1.1; font-variant-numeric: tabular-nums; }
.kpi b.kpi-text { font-size: 18px; }
.kpi small { font-size: 12px; color: var(--muted-ink); min-height: 1.2em; }
.kpi.is-warn b { color: var(--preparing); }
.kpi.is-error b { color: var(--error); }
.kicker { font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--muted-ink); }

.side-card { padding: 14px 16px; display: grid; gap: 10px; }
.side-card > h2.kicker { margin: 0; }
.side-card .relay { display: grid; gap: 8px; }
/* The selection form holds the list, the note and the button: without this the rows it took in
   lose the panel's rhythm and read as one block of text. */
.side-card .relay form { display: grid; gap: 8px; }
.side-card .relay .meta, .side-card .maintenance .meta { margin: 0; font-size: 13px; color: var(--muted-ink); }

/* Who is waiting. A backlog is read before it is drained, so the names sit above the button
   rather than behind a disclosure: the decision this panel asks for cannot be made without them. */
.backlog { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.backlog li {
  display: grid;
  gap: 2px;
  padding: 8px 10px;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font-size: 13px;
  overflow-wrap: anywhere;
}
.backlog li b { font-weight: 700; }
/* The tick box is the gesture, so the whole row is its label: on a phone, a 13px checkbox is not
   a target, and the name next to it is. */
.backlog-pick { display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: start; cursor: pointer; }
.backlog-pick input { margin: 2px 0 0; width: 18px; height: 18px; accent-color: var(--navy); flex: none; }
.backlog-pick > span { display: grid; gap: 2px; min-width: 0; }
.backlog .backlog-mail { color: var(--muted-ink); }
.side-card .relay .backlog .meta { font-size: 12px; }
.side-card .maintenance { display: grid; gap: 8px; }
.side-card .maintenance p { margin: 0; }
.side-card details.expert { margin-top: 0; }
.btn-block { width: 100%; }
.facts-list { margin: 0; display: grid; gap: 6px; }
.facts-list div { display: flex; justify-content: space-between; gap: 12px; font-size: 13px; }
.facts-list dt { color: var(--muted-ink); flex: none; }
.facts-list dd { margin: 0; text-align: right; min-width: 0; overflow-wrap: anywhere; }
.facts-list .mono { font-size: 12px; }
.is-missing { color: var(--error); font-weight: 600; }
.side-more { font-size: 13px; font-weight: 600; text-decoration: none; }

