/* ==========================================================================
   tenrix-platform console
   Built on the dark green token set from style.css. That file established the
   palette and the terminal/log language; this extends it into the surfaces a
   migration console needs — a sidebar shell, data tables, the state pills the
   ledger reports in, and the two-sided source/target language of §2.2.
   ========================================================================== */

:root {
  /* ---- inherited from style.css ---- */
  --bg: #0C0F0E;
  --bg-grain: #0F1312;
  --panel: #14181A;
  --panel-raised: #181D1F;
  --border: #232B2C;
  --border-soft: #1B2224;
  --text: #E7EBE8;
  --text-muted: #8B9694;
  --text-dim: #5C6664;
  --accent: #33D296;
  --accent-dim: #1E7A57;
  --accent-wash: rgba(51, 210, 150, 0.10);
  --amber: #F0B429;
  --red: #EA5F5A;
  --font-display: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace;
  --radius: 10px;

  /* ---- extensions ---- */
  --accent-bright: #3EE3A3;
  --accent-deep: #0F3D2C;
  --amber-wash: rgba(240, 180, 41, 0.10);
  --red-wash: rgba(234, 95, 90, 0.10);
  --blue: #5AA9EA;            /* the target side — read/write, distinct from source */
  --blue-wash: rgba(90, 169, 234, 0.10);
  --violet: #A78BFA;
  --violet-wash: rgba(167, 139, 250, 0.10);

  --radius-sm: 7px;
  --radius-lg: 14px;
  --sidebar-w: 244px;
  --shadow-panel: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-pop: 0 12px 32px rgba(0, 0, 0, 0.55);
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
}

* { box-sizing: border-box; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

html, body {
  margin: 0;
  padding: 0;
  min-height: 100vh;
  background:
    radial-gradient(ellipse 900px 500px at 12% -12%, rgba(51, 210, 150, 0.055), transparent 62%),
    radial-gradient(ellipse 700px 420px at 88% 0%, rgba(90, 169, 234, 0.035), transparent 60%),
    var(--bg);
  background-attachment: fixed;
  color: var(--text);
  font-family: var(--font-display);
  font-size: 14px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

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

::selection { background: var(--accent-dim); color: #06110C; }

/* ---------- scrollbars ---------- */

* { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 6px;
  border: 2px solid var(--bg);
}
*::-webkit-scrollbar-thumb:hover { background: #2E393A; }

/* ==========================================================================
   Shell — sidebar + main
   ========================================================================== */

.shell { display: flex; min-height: 100vh; }

.sidebar {
  position: fixed;
  inset: 0 auto 0 0;
  width: var(--sidebar-w);
  display: flex;
  flex-direction: column;
  background: linear-gradient(180deg, var(--panel) 0%, var(--bg-grain) 100%);
  border-right: 1px solid var(--border-soft);
  z-index: 40;
}

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 20px 18px;
  border-bottom: 1px solid var(--border-soft);
}

.brand-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 34px;
  width: 34px;
  height: 34px;
  border-radius: 9px;
  border: 1px solid var(--accent-dim);
  background: linear-gradient(145deg, var(--accent-deep), var(--panel-raised));
  color: var(--accent);
  box-shadow: inset 0 1px 0 rgba(51, 210, 150, 0.16);
}

.brand-text { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.brand-name { font-weight: 700; font-size: 14.5px; letter-spacing: -0.01em; }
.brand-sub {
  font-size: 11px;
  color: var(--text-dim);
  font-family: var(--font-mono);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.nav { flex: 1; overflow-y: auto; padding: 14px 10px; }

.nav-section {
  margin: 0 8px 7px;
  padding-top: 12px;
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--text-dim);
}
.nav-section:first-child { padding-top: 0; }

.nav-link {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  margin-bottom: 2px;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font-size: 13.5px;
  font-weight: 500;
  transition: background 130ms var(--ease), color 130ms var(--ease);
  position: relative;
}

.nav-link:hover { background: var(--panel-raised); color: var(--text); }

.nav-link.is-active {
  background: var(--accent-wash);
  color: var(--accent);
  font-weight: 600;
}

/* The active marker reads as a channel indicator rather than a border, which
   keeps the eye on the label instead of the edge. */
.nav-link.is-active::before {
  content: "";
  position: absolute;
  left: -10px;
  top: 50%;
  transform: translateY(-50%);
  width: 3px;
  height: 17px;
  border-radius: 0 3px 3px 0;
  background: var(--accent);
}

.nav-link svg { flex: 0 0 17px; opacity: 0.85; }
.nav-link.is-active svg { opacity: 1; }

.nav-badge {
  margin-left: auto;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--border);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 10.5px;
}
.nav-link.is-active .nav-badge { background: var(--accent-dim); color: #06110C; }

.sidebar-foot {
  border-top: 1px solid var(--border-soft);
  padding: 12px 12px 14px;
}

.user-chip {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px;
  border-radius: var(--radius-sm);
  transition: background 130ms var(--ease);
}
.user-chip:hover { background: var(--panel-raised); }

.avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 30px;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--accent-deep);
  border: 1px solid var(--accent-dim);
  color: var(--accent);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.user-meta { min-width: 0; line-height: 1.3; }
.user-name {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.user-role {
  font-size: 10.5px;
  color: var(--text-dim);
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  transition: background 130ms var(--ease), color 130ms var(--ease);
}
.icon-btn:hover { background: var(--panel); color: var(--text); border-color: var(--border); }

/* ---------- main ---------- */

.main {
  flex: 1;
  min-width: 0;
  margin-left: var(--sidebar-w);
  display: flex;
  flex-direction: column;
}

.topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  padding: 0 26px;
  height: 58px;
  background: rgba(12, 15, 14, 0.86);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border-soft);
}

.crumbs {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  color: var(--text-dim);
  min-width: 0;
}
.crumbs a { color: var(--text-muted); }
.crumbs a:hover { color: var(--text); }
.crumbs .sep { color: var(--text-dim); opacity: 0.6; }
.crumbs .current { color: var(--text); font-weight: 500; }

.topbar-actions { display: flex; align-items: center; gap: 10px; }

.content { padding: 26px; max-width: 1500px; width: 100%; }

.page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
  margin-bottom: 22px;
  flex-wrap: wrap;
}

.page-title {
  margin: 0 0 5px;
  font-size: 23px;
  font-weight: 680;
  letter-spacing: -0.02em;
}

.page-sub { margin: 0; color: var(--text-muted); font-size: 13.5px; max-width: 74ch; }

/* ---------- sub-navigation (tabs within a project) ---------- */

/* A segmented control: one inset track holding the tabs, with the active one
   raised out of it. A bare underline was too faint to answer "where am I" at a
   glance, and a row of filled buttons read as actions that navigate away —
   wrong for switching between views of the same project. */
.subnav {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  margin-bottom: 22px;
  padding: 4px;
  max-width: 100%;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--bg-grain);
  overflow-x: auto;
  scrollbar-width: none;
}
.subnav::-webkit-scrollbar { display: none; }

.subnav-tab {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 15px;
  border-radius: 6px;
  border: 1px solid transparent;
  color: var(--text-muted);
  font-size: 13.5px;
  font-weight: 550;
  white-space: nowrap;
  transition: background 130ms var(--ease), color 130ms var(--ease),
              border-color 130ms var(--ease);
}

.subnav-tab:hover {
  background: var(--panel-raised);
  color: var(--text);
}

/* The active tab sits proud of the track: lighter ground, a real border and a
   hairline shadow, so it reads as the selected surface rather than a hover. */
.subnav-tab.is-active {
  background: var(--panel-raised);
  border-color: var(--border);
  color: var(--accent);
  font-weight: 640;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}

.subnav-tab.is-active:hover { color: var(--accent); }

/* A short accent rule under the label, so the active tab is still legible
   without relying on colour alone. */
.subnav-tab.is-active .subnav-label {
  box-shadow: inset 0 -2px 0 var(--accent);
  padding-bottom: 2px;
}

/* A stage that is not built yet. Shown rather than hidden so the shape of the
   whole job is visible, but plainly not a place you can go. */
.subnav-tab.is-pending {
  color: var(--text-dim);
  cursor: default;
  opacity: 0.55;
}
.subnav-tab.is-pending:hover { background: transparent; color: var(--text-dim); }

/* The chevron between stages. Decorative — the order is the information. */
.subnav-sep {
  color: var(--text-dim);
  opacity: 0.4;
  font-size: 12px;
  padding: 0 1px;
  user-select: none;
  align-self: center;
}

/* Pushes Settings to the far end: it is configuration, not a stage. */
.subnav-spacer { flex: 1; min-width: 12px; }

/* An unresolved item on a tab you are not currently looking at. */
.subnav-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--amber);
  flex: 0 0 6px;
  box-shadow: 0 0 0 2px var(--amber-wash);
}

@media (max-width: 620px) {
  .subnav { display: flex; width: 100%; }
  .subnav-tab { flex: 1; justify-content: center; padding: 7px 10px; }
}

/* ==========================================================================
   Panels, cards, tiles
   ========================================================================== */

.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-panel);
}

/* A panel whose whole subject is a problem — a service that has stopped
   watching, an action nobody has answered. Tinted rather than badged, because
   the point is that the panel is read before the ones around it. Used
   sparingly: two red panels on a page is the same as none. */
.panel-danger {
  border-color: rgba(234, 95, 90, 0.4);
  background: var(--red-wash);
}
.panel-warning {
  border-color: rgba(240, 180, 41, 0.4);
  background: var(--amber-wash);
}

/* A row that is a form: one label, one control, one button, on a line. */
.inline-form {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 15px 18px;
  border-bottom: 1px solid var(--border-soft);
}

.panel-title {
  margin: 0;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-muted);
}

.panel-body { padding: 18px; }
.panel-body.flush { padding: 0; }

.grid { display: grid; gap: 16px; }
/* Cards in a grid stretch to the tallest by default, which pads a short table
   with empty space to match a long one. Each should be its own height. */
.grid > .panel { align-self: start; }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* Five tiles rather than four when a run found archives. Separate from
   `grid-4` so nothing that already uses four columns changes width. */
.grid-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.grid-main { grid-template-columns: minmax(0, 1fr) 340px; align-items: start; }

@media (max-width: 1180px) {
  .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid-5 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .grid-main { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 780px) {
  .grid-2, .grid-3 { grid-template-columns: minmax(0, 1fr); }
  /* Tiles stay two-up on a phone. One column turns four KPIs into a screen
     and a half of scrolling, and the whole point of a tile is that it is
     read at a glance rather than scrolled to. */
  .grid-4, .grid-5 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 380px) {
  .grid-4, .grid-5 { grid-template-columns: minmax(0, 1fr); }
}

/* ---------- stat tile ---------- */

.tile {
  position: relative;
  padding: 17px 18px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  transition: border-color 150ms var(--ease), transform 150ms var(--ease);
}
.tile:hover { border-color: #2C3637; transform: translateY(-1px); }

/* A hairline of colour rather than a filled block: the number is the content,
   the accent is only a category cue. */
.tile::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background: var(--accent);
  opacity: 0.55;
}
.tile.is-amber::after { background: var(--amber); }
.tile.is-red::after { background: var(--red); }
.tile.is-blue::after { background: var(--blue); }
.tile.is-muted::after { background: var(--border); }

.tile-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  margin-bottom: 9px;
}

.tile-value {
  font-family: var(--font-mono);
  font-size: 27px;
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.tile-foot { margin-top: 7px; font-size: 12px; color: var(--text-muted); }

/* ---------- section heading ---------- */

/* A page with several stacked panels needs grouping, or every section reads as
   equally important. This separates them without adding another box. */
.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  margin: 28px 0 14px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border-soft);
}

.section-title {
  margin: 0;
  font-size: 15px;
  font-weight: 640;
  letter-spacing: -0.01em;
  color: var(--text);
}

.section-note {
  font-size: 12.5px;
  color: var(--text-dim);
  text-align: right;
}

/* ==========================================================================
   State pills — the ledger's vocabulary (§4.3)
   ========================================================================== */

.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2.5px 9px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg-grain);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.pill .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 6px;
}

.pill.ok, .pill.verified, .pill.complete, .pill.healthy, .pill.active {
  color: var(--accent); border-color: var(--accent-dim); background: var(--accent-wash);
}
.pill.busy, .pill.transferring, .pill.running, .pill.pending, .pill.degraded,
.pill.warn {
  color: var(--amber); border-color: rgba(240, 180, 41, 0.4); background: var(--amber-wash);
}
.pill.bad, .pill.failed, .pill.conflict, .pill.error {
  color: var(--red); border-color: rgba(234, 95, 90, 0.4); background: var(--red-wash);
}
.pill.info, .pill.queued, .pill.mapped {
  color: var(--blue); border-color: rgba(90, 169, 234, 0.4); background: var(--blue-wash);
}
.pill.muted, .pill.skipped, .pill.draft, .pill.unknown {
  color: var(--text-dim);
}

/* A pulsing dot for genuinely live states only — a page of blinking rows is
   noise, not information. */
.pill.busy .dot, .pill.transferring .dot, .pill.running .dot {
  animation: pulse 1.4s infinite var(--ease);
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }

/* ---------- side badges: source is read-only, target is read-write ---------- */

.side-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 500;
}
.side-badge.source {
  color: var(--accent);
  background: var(--accent-wash);
  border: 1px solid var(--accent-dim);
}
.side-badge.target {
  color: var(--blue);
  background: var(--blue-wash);
  border: 1px solid rgba(90, 169, 234, 0.4);
}

/* ==========================================================================
   Tables
   ========================================================================== */

.table-wrap { overflow-x: auto; }

table.data {
  width: 100%;
  border-collapse: collapse;
  font-size: 13.5px;
}

table.data thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 10px 16px;
  text-align: left;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-dim);
  background: var(--bg-grain);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

table.data tbody td {
  padding: 12px 16px;
  border-bottom: 1px solid var(--border-soft);
  vertical-align: middle;
}

table.data tbody tr { transition: background 120ms var(--ease); }
table.data tbody tr:hover { background: var(--panel-raised); }
table.data tbody tr:last-child td { border-bottom: none; }

table.data td.num,
table.data th.num {
  text-align: right;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

.cell-primary { font-weight: 560; color: var(--text); }

/* Microsoft product marks. Sized to the text they sit beside, and never
   shrunk by a flex parent — a squashed brand mark reads as a rendering bug. */
.wl-icon {
  width: 17px;
  height: 17px;
  flex: 0 0 17px;
  display: inline-block;
  vertical-align: -3px;
}

/* On the dark ground the marks keep their brand colours, but the darkest of
   them (SharePoint teal, Outlook navy) need a touch of lift to stay legible. */
.wl-icon { filter: saturate(1.08) brightness(1.06); }
.cell-sub { font-size: 12px; color: var(--text-dim); margin-top: 2px; }
.mono { font-family: var(--font-mono); font-size: 12.5px; }

.row-link { color: inherit; display: block; }
.row-link:hover { color: var(--accent); }

.empty {
  padding: 52px 24px;
  text-align: center;
  color: var(--text-dim);
}
.empty-icon { margin-bottom: 12px; opacity: 0.4; }
.empty-title { color: var(--text-muted); font-weight: 600; margin-bottom: 5px; }
.empty-hint { font-size: 13px; max-width: 46ch; margin: 0 auto; }

/* ==========================================================================
   Forms
   ========================================================================== */

.field { margin-bottom: 16px; }

.field label,
.field-label {
  display: block;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text-muted);
  margin-bottom: 7px;
}

.req { color: var(--accent); margin-left: 2px; }

input[type="text"], input[type="email"], input[type="number"],
input[type="password"], input[type="search"], input[type="url"],
input[type="datetime-local"], input[type="date"],
select, textarea {
  width: 100%;
  background: var(--bg-grain);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 9px 12px;
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 13px;
  transition: border-color 130ms var(--ease), box-shadow 130ms var(--ease);
}

select {
  font-family: var(--font-display);
  font-size: 13.5px;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238B9694' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
  padding-right: 32px;
}
select[multiple] { background-image: none; padding-right: 12px; }

textarea { resize: vertical; line-height: 1.55; font-family: var(--font-display); }

input::placeholder, textarea::placeholder { color: var(--text-dim); }

input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent-dim);
  box-shadow: 0 0 0 3px var(--accent-wash);
}

input[type="checkbox"] {
  width: 15px;
  height: 15px;
  accent-color: var(--accent);
  vertical-align: -2px;
}

.help { font-size: 12px; color: var(--text-dim); margin-top: 6px; line-height: 1.5; }
.field-error { font-size: 12px; color: var(--red); margin-top: 6px; }
input.has-error, select.has-error, textarea.has-error { border-color: var(--red); }

.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 18px; }
.form-grid .span-2 { grid-column: 1 / -1; }
@media (max-width: 780px) { .form-grid { grid-template-columns: 1fr; } }

.form-section {
  margin: 26px 0 14px;
  padding-top: 18px;
  border-top: 1px solid var(--border-soft);
}
.form-section:first-child { margin-top: 0; padding-top: 0; border-top: none; }
.form-section-title { font-size: 13px; font-weight: 640; margin: 0 0 3px; }
.form-section-hint { font-size: 12.5px; color: var(--text-dim); margin: 0 0 16px; }

.form-actions {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
  padding-top: 18px;
  margin-top: 22px;
  border-top: 1px solid var(--border-soft);
}

/* ---------- buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 9px 15px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--panel-raised);
  color: var(--text);
  font-family: var(--font-display);
  font-size: 13.5px;
  font-weight: 560;
  cursor: pointer;
  white-space: nowrap;
  transition: background 130ms var(--ease), border-color 130ms var(--ease),
              transform 90ms var(--ease), opacity 130ms var(--ease);
}

.btn:hover { background: #1E2426; border-color: #2E393A; color: var(--text); }
.btn:active { transform: scale(0.985); }

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #06110C;
  font-weight: 640;
}
.btn-primary:hover { background: var(--accent-bright); border-color: var(--accent-bright); color: #06110C; }

.btn-danger { color: var(--red); border-color: rgba(234, 95, 90, 0.35); }
.btn-danger:hover { background: var(--red-wash); border-color: var(--red); color: var(--red); }

.btn-ghost { background: transparent; border-color: transparent; color: var(--text-muted); }
.btn-ghost:hover { background: var(--panel-raised); color: var(--text); }

.btn-sm { padding: 6px 11px; font-size: 12.5px; }

.btn:disabled, .btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}

/* ---------- filter bar ---------- */

.filter-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 16px;
}

.search-field { position: relative; flex: 1 1 260px; max-width: 400px; }
.search-field svg {
  position: absolute;
  left: 11px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-dim);
  pointer-events: none;
}
.search-field input { padding-left: 34px; font-family: var(--font-display); font-size: 13.5px; }

.segmented {
  display: flex;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.seg-btn {
  padding: 8px 13px;
  background: var(--bg-grain);
  border: none;
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 12px;
  cursor: pointer;
  transition: background 120ms var(--ease), color 120ms var(--ease);
}
.seg-btn + .seg-btn { border-left: 1px solid var(--border); }
.seg-btn:hover { color: var(--text); }
.seg-btn.is-active { background: var(--accent-wash); color: var(--accent); }

/* ==========================================================================
   Progress
   ========================================================================== */

.progress {
  height: 6px;
  border-radius: 999px;
  background: var(--bg-grain);
  border: 1px solid var(--border-soft);
  overflow: hidden;
}
.progress-bar {
  height: 100%;
  background: linear-gradient(90deg, var(--accent-dim), var(--accent));
  border-radius: 999px;
  transition: width 420ms var(--ease);
}
.progress-bar.is-amber { background: linear-gradient(90deg, #9A7415, var(--amber)); }
.progress-bar.is-red { background: linear-gradient(90deg, #8E3936, var(--red)); }

.progress-legend {
  display: flex;
  justify-content: space-between;
  margin-top: 7px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-dim);
}

/* A segmented bar showing the ledger's whole distribution at a glance. */
.state-bar {
  display: flex;
  height: 8px;
  border-radius: 999px;
  overflow: hidden;
  background: var(--bg-grain);
  border: 1px solid var(--border-soft);
}
.state-bar span { display: block; height: 100%; transition: width 420ms var(--ease); }
.state-bar .s-verified { background: var(--accent); }
.state-bar .s-transferred { background: var(--accent-dim); }
.state-bar .s-transferring { background: var(--amber); }
.state-bar .s-queued { background: var(--blue); }
.state-bar .s-failed { background: var(--red); }
.state-bar .s-other { background: var(--border); }

.legend { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 12px; }
.legend-item {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text-muted);
}
.legend-swatch { width: 9px; height: 9px; border-radius: 2px; }

/* A capped, scrolling region. The dashboard's activity feed grows without
   bound, and letting it set the panel height makes every other tile on the row
   stretch to match. */
.scroll-y {
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Room for the timeline rail, which sits in the left padding. */
  padding-right: 4px;
}

/* A soft fade at the foot, so a clipped list reads as "there is more" rather
   than "this ended abruptly". */
.scroll-fade { position: relative; }
.scroll-fade::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 34px;
  pointer-events: none;
  background: linear-gradient(to bottom, transparent, var(--panel));
}

/* ==========================================================================
   Notices
   ========================================================================== */

.notice {
  display: flex;
  gap: 11px;
  padding: 13px 15px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--panel-raised);
  font-size: 13.5px;
  line-height: 1.55;
  margin-bottom: 14px;
}
.notice svg { flex: 0 0 17px; margin-top: 1px; }
.notice-title { font-weight: 620; margin-bottom: 3px; }
.notice-body { color: var(--text-muted); }

.notice.ok { border-color: var(--accent-dim); background: var(--accent-wash); }
.notice.ok svg, .notice.ok .notice-title { color: var(--accent); }
.notice.warn { border-color: rgba(240, 180, 41, 0.4); background: var(--amber-wash); }
.notice.warn svg, .notice.warn .notice-title { color: var(--amber); }
.notice.danger { border-color: rgba(234, 95, 90, 0.4); background: var(--red-wash); }
.notice.danger svg, .notice.danger .notice-title { color: var(--red); }
.notice.info { border-color: rgba(90, 169, 234, 0.4); background: var(--blue-wash); }
.notice.info svg, .notice.info .notice-title { color: var(--blue); }

/* Toasts from django.contrib.messages */
.toasts {
  position: fixed;
  right: 22px;
  bottom: 22px;
  z-index: 90;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 380px;
}
.toast {
  padding: 12px 15px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--panel-raised);
  box-shadow: var(--shadow-pop);
  font-size: 13.5px;
  animation: toastIn 260ms var(--ease);
}
@keyframes toastIn {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
.toast.success { border-color: var(--accent-dim); color: var(--accent); }
.toast.error { border-color: rgba(234, 95, 90, 0.45); color: var(--red); }
.toast.warning { border-color: rgba(240, 180, 41, 0.45); color: var(--amber); }

/* ==========================================================================
   Timeline — audit and activity
   ========================================================================== */

.timeline { position: relative; padding-left: 20px; }
.timeline::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 5px;
  bottom: 5px;
  width: 1px;
  background: var(--border-soft);
}

.tl-item { position: relative; padding-bottom: 17px; }
.tl-item:last-child { padding-bottom: 0; }
.tl-item::before {
  content: "";
  position: absolute;
  left: -20px;
  top: 5px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--border);
  border: 2px solid var(--bg);
}
.tl-item.is-ok::before { background: var(--accent); }
.tl-item.is-warn::before { background: var(--amber); }
.tl-item.is-bad::before { background: var(--red); }

.tl-text { font-size: 13px; color: var(--text); line-height: 1.5; }
.tl-meta {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-dim);
  margin-top: 3px;
}

/* ==========================================================================
   Login
   ========================================================================== */

.auth-wrap {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 26px;
}

.auth-card {
  width: 100%;
  max-width: 408px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 34px 32px 28px;
  box-shadow: var(--shadow-pop);
}

.auth-brand { display: flex; align-items: center; gap: 12px; margin-bottom: 26px; }
.auth-brand .brand-mark { width: 40px; height: 40px; flex-basis: 40px; }
.auth-title { margin: 0 0 5px; font-size: 19px; font-weight: 680; letter-spacing: -0.02em; }
.auth-sub { margin: 0 0 24px; color: var(--text-muted); font-size: 13.5px; }

.sso-btn {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 11px 16px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg-grain);
  color: var(--text);
  font-size: 13.5px;
  font-weight: 560;
  cursor: pointer;
  transition: background 130ms var(--ease), border-color 130ms var(--ease);
}
.sso-btn:hover { background: var(--panel-raised); border-color: #2E393A; }

.auth-divider {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 22px 0;
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.auth-divider::before, .auth-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border-soft);
}

.auth-foot {
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--border-soft);
  text-align: center;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-dim);
}

/* ==========================================================================
   Utilities
   ========================================================================== */

.stack { display: flex; flex-direction: column; }
.row { display: flex; align-items: center; }
.gap-4 { gap: 4px; } .gap-6 { gap: 6px; } .gap-8 { gap: 8px; }
.gap-10 { gap: 10px; } .gap-14 { gap: 14px; } .gap-16 { gap: 16px; }
.between { justify-content: space-between; }
.wrap { flex-wrap: wrap; }
.grow { flex: 1; }
.mt-0 { margin-top: 0; } .mt-8 { margin-top: 8px; } .mt-14 { margin-top: 14px; }
.mt-20 { margin-top: 20px; } .mt-24 { margin-top: 24px; }
.mb-0 { margin-bottom: 0; } .mb-8 { margin-bottom: 8px; } .mb-14 { margin-bottom: 14px; }
.mb-20 { margin-bottom: 20px; }
.muted { color: var(--text-muted); }
.dim { color: var(--text-dim); }
.small { font-size: 12.5px; }
.tiny { font-size: 11.5px; }
.strong { font-weight: 620; }
.nowrap { white-space: nowrap; }
.right { text-align: right; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hidden { display: none !important; }
.accent { color: var(--accent); }
.amber { color: var(--amber); }
.danger { color: var(--red); }

.kbd {
  padding: 1.5px 6px;
  border-radius: 4px;
  border: 1px solid var(--border);
  background: var(--bg-grain);
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
}

.divider { height: 1px; background: var(--border-soft); margin: 18px 0; }

/* HTMX — a quiet indication that something is in flight. */
.htmx-indicator { opacity: 0; transition: opacity 150ms var(--ease); }
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { opacity: 1; }
.htmx-swapping { opacity: 0.4; transition: opacity 90ms var(--ease); }

/* ---------- responsive shell ---------- */

/* The slide-in drawer that used to live here is gone. Below 940px the nav is
   a bottom tab bar (see "mobile: a tab bar, not a drawer"), and leaving the
   drawer rules in place kept a second, conflicting navigation alive under the
   hamburger — two menus that disagree about what is selected. */



/* -- live progress -------------------------------------------------------
   A long transfer has to show movement. A page that looks identical for ten
   minutes is indistinguishable from one that has hung, and that is how a
   working migration gets killed by someone who reasonably assumed it was
   stuck. */

.live-banner {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 9px 14px;
  margin-bottom: 14px;
  border: 1px solid rgba(51, 210, 150, 0.28);
  background: var(--accent-wash);
  border-radius: var(--radius);
  font-size: 12.5px;
  color: var(--text);
}

.live-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 0 rgba(51, 210, 150, 0.5);
  animation: live-pulse 1.8s ease-out infinite;
  flex: none;
}

@keyframes live-pulse {
  70%  { box-shadow: 0 0 0 7px rgba(51, 210, 150, 0); }
  100% { box-shadow: 0 0 0 0 rgba(51, 210, 150, 0); }
}

.prog {
  height: 4px;
  margin-top: 6px;
  border-radius: 3px;
  background: var(--border-soft);
  overflow: hidden;
}

.prog-fill {
  height: 100%;
  border-radius: 3px;
  background: var(--accent-dim);
  transition: width 0.6s ease;
}

/* Moving right now — a sheen so progress is visible even between polls. */
.prog-fill.is-live {
  background: linear-gradient(90deg, var(--accent-dim), var(--accent), var(--accent-dim));
  background-size: 200% 100%;
  animation: prog-sheen 1.4s linear infinite;
}

@keyframes prog-sheen {
  to { background-position: -200% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .live-dot, .prog-fill.is-live { animation: none; }
}











/* -- licence holders -----------------------------------------------------
   Who holds a fully-assigned product, shown beside the shortfall it is
   causing. Freeing a unit takes the product away from a live account — and
   their mailbox with it, if that is what provisioned it — so the row states
   the consequence rather than implying it. */

.holders {
  margin-top: 10px;
  padding: 12px 14px;
  border: 1px solid var(--border-soft);
  border-radius: 9px;
  background: var(--panel);
}

.holders-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 9px;
}

.holders-title { font-size: 12.5px; color: var(--text); }

.holders-list { display: grid; gap: 4px; }

.holder {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 10px;
  border-radius: 7px;
  background: var(--panel-raised);
}

.holder-who { min-width: 0; }
.holder-name { font-size: 12px; color: var(--text); }
.holder-upn { line-height: 1.35; word-break: break-all; }

/* -- licence picker ------------------------------------------------------
   The target is a different estate from the source: someone may need a
   product they never had, and a source SKU may not exist here at all. So the
   plan is editable, and each option says whether a unit is actually free —
   ticking one that is not is a plan that fails at the API. */

.ldialog { max-width: 480px; }

.lic-list {
  display: grid;
  gap: 6px;
  margin-bottom: 18px;
  max-height: 300px;
  overflow-y: auto;
}

.lic-option {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 10px 12px;
  border: 1px solid var(--border-soft);
  border-radius: 8px;
  cursor: pointer;
  transition: border-color 0.12s ease, background 0.12s ease;
}

.lic-option:hover { border-color: var(--border); background: var(--panel); }

.lic-option:has(input:checked) {
  border-color: var(--accent-dim);
  background: var(--accent-wash);
}

/* Selectable, but the tenant has no unit left — so the consequence is stated
   rather than the option being hidden. A consultant may be about to buy one. */
.lic-option.is-none-left { opacity: 0.75; }

.lic-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.lic-name { font-size: 12.5px; color: var(--text); }
.lic-meta { display: flex; gap: 5px; flex-wrap: wrap; }

.btn-link {
  background: none;
  border: 0;
  padding: 0;
  margin-top: 4px;
  color: var(--accent);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
  font: inherit;
  font-size: 11.5px;
}

.btn-link:hover { color: var(--accent-bright); }

/* -- confirmation dialog -------------------------------------------------
   Replaces the browser's `confirm()`, which shows the page URL, cannot be
   styled and collapses every line break into one sentence. On the screen
   where somebody stops a customer's mail, that is the wrong tone and too
   little information.

   Built on <dialog>, so focus trapping, Escape and the backdrop come from the
   browser rather than from script. */

.cdialog {
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel-raised);
  color: var(--text);
  padding: 0;
  max-width: 440px;
  width: calc(100% - 32px);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
}

.cdialog::backdrop {
  background: rgba(6, 9, 8, 0.62);
  backdrop-filter: blur(2px);
}

.cdialog-inner { padding: 22px 22px 18px; margin: 0; }

.cdialog-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  margin-bottom: 13px;
  color: var(--amber);
  background: rgba(240, 180, 41, 0.12);
}

/* Anything irreversible — removing a domain, revoking access — is red, so the
   two kinds of confirmation never look alike. */
.cdialog-icon.is-danger {
  color: var(--red);
  background: var(--red-wash);
}

.cdialog-title {
  font-size: 15px;
  font-weight: 600;
  margin: 0 0 7px;
  color: var(--text);
}

.cdialog-body {
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--text-muted);
  margin: 0 0 18px;
}

.cdialog-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}

@media (prefers-reduced-motion: no-preference) {
  .cdialog[open] { animation: cdialog-in 0.14s ease-out; }
}

@keyframes cdialog-in {
  from { opacity: 0; transform: translateY(-6px) scale(0.98); }
}

/* -- cutover -------------------------------------------------------------
   The one page in the product where the actions are not reversible. Removing
   a domain from the source tenant stops mail immediately, and the way back is
   hours of DNS — so the point of no return is marked as loudly as the design
   allows, and the steps are ticked by a person rather than run on a timer. */

.gng-list { display: grid; }

.gng {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 11px 20px;
  border-bottom: 1px solid var(--border-soft);
}

.gng:last-child { border-bottom: 0; }

.gng-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  flex: none;
  margin-top: 1px;
}

.gng.is-ok .gng-mark {
  color: var(--accent);
  background: var(--accent-wash);
}

.gng.is-bad .gng-mark {
  color: var(--red);
  background: var(--red-wash);
}

.gng-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.gng-label { font-size: 12.5px; color: var(--text); }
.gng-detail { color: var(--text-muted); line-height: 1.45; }

/* Day-one warnings: nobody can migrate these, so forewarning is the whole
   mitigation. Presented as cards to be read, not a list to be skimmed. */
.warn-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 10px;
}

.warn-card {
  padding: 12px 14px;
  border: 1px solid rgba(240, 180, 41, 0.22);
  border-radius: 9px;
  background: rgba(240, 180, 41, 0.05);
}

.warn-title { font-size: 12.5px; color: var(--amber); margin-bottom: 5px; }
.warn-detail { color: var(--text-muted); line-height: 1.5; }

.step-list { display: grid; }

.cstep {
  display: grid;
  grid-template-columns: 34px 1fr auto;
  gap: 12px;
  padding: 13px 20px;
  border-bottom: 1px solid var(--border-soft);
  align-items: start;
}

.cstep:last-child { border-bottom: 0; }

.cstep-num {
  font-size: 12px;
  color: var(--text-dim);
  text-align: right;
  padding-top: 2px;
}

.cstep.is-done { background: rgba(51, 210, 150, 0.04); }
.cstep.is-done .cstep-num { color: var(--accent); }

/* The step after which there is no going back. */
.cstep.is-nrp {
  border-left: 3px solid var(--red);
  background: var(--red-wash);
}

.cstep-title {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 13px;
  color: var(--text);
  margin-bottom: 4px;
}

.cstep-detail { line-height: 1.5; }
.cstep-note { margin-top: 4px; }
.cstep-action { flex: none; }

/* -- readiness -----------------------------------------------------------
   The three decisions as one picture, on whichever of the three pages you are
   on. Without it a consultant has to visit each tab to learn where they
   stand, and the tab they forget is the one that quietly never migrates. */

.ready-row {
  display: flex;
  align-items: stretch;
  gap: 6px;
  flex-wrap: wrap;
}

.ready-step {
  display: flex;
  align-items: center;
  gap: 9px;
  flex: 1 1 170px;
  min-width: 0;
  padding: 11px 13px;
  border: 1px solid var(--border-soft);
  border-radius: 9px;
  background: var(--panel-raised);
  text-decoration: none;
  transition: border-color 0.12s ease, background 0.12s ease;
}

.ready-step:hover { border-color: var(--border); }

/* The page you are on. These cards are the only navigation between the three
   scopes now, so this has to be unmistakable rather than a slight tint. */
.ready-step.is-current {
  border-color: var(--accent);
  background: var(--accent-wash);
  box-shadow: inset 0 0 0 1px rgba(51, 210, 150, 0.22);
  cursor: default;
}

.ready-step.is-current .ready-name { color: var(--accent); }

/* A card you can move to, as opposed to the one you are on. */
.ready-step:not(.is-current):hover {
  border-color: var(--accent-dim);
  background: var(--panel-raised);
}

.ready-step.is-done { border-color: rgba(51, 210, 150, 0.26); }

/* Waiting on a person — the state that matters, because it is the one that
   silently costs a workload its migration. */
.ready-step.is-waiting { border-color: rgba(240, 180, 41, 0.3); }

/* Nothing loaded yet is not "done": nobody has looked at it. */
.ready-step.is-empty { opacity: 0.72; }

.ready-icon { display: inline-flex; flex: none; }
.ready-icon .wl-icon { width: 17px; height: 17px; }

.ready-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.ready-name { font-size: 12.5px; color: var(--text); }
.ready-note { line-height: 1.3; }

.ready-check { color: var(--accent); margin-left: auto; flex: none; }

.ready-sep {
  align-self: center;
  color: var(--text-dim);
  font-size: 15px;
  flex: none;
}

@media (max-width: 700px) {
  /* The arrows imply a left-to-right order that stacking breaks. */
  .ready-sep { display: none; }
}

/* -- exclusions ----------------------------------------------------------
   What did not migrate, grouped by the object it belongs to. Read as "here is
   what happened to Megan's OneDrive" rather than "here are forty caveats".

   The kinds are visually distinct because they need different responses: a
   blocked or failed line is work, an impossible one is a fact about
   tenant-to-tenant migration that the customer still has to be told. */

.excl-list { display: grid; }

.excl {
  padding: 13px 20px;
  border-bottom: 1px solid var(--border-soft);
}

.excl:last-child { border-bottom: 0; }

.excl-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.excl-icon { display: inline-flex; flex: none; }
.excl-icon .wl-icon { width: 15px; height: 15px; }

.excl-subject {
  font-size: 12.5px;
  color: var(--text);
  min-width: 0;
  overflow-wrap: anywhere;
}

.excl-head .pill { margin-left: auto; flex: none; }

.excl-lines {
  list-style: none;
  display: grid;
  gap: 5px;
  margin: 0;
  padding-left: 23px;
}

.excl-line {
  display: flex;
  gap: 9px;
  align-items: baseline;
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.5;
}

.excl-kind {
  flex: none;
  min-width: 108px;
  padding: 1px 7px;
  border-radius: 4px;
  border: 1px solid var(--border);
  text-align: center;
  color: var(--text-dim);
}

/* Impossible: nobody can fix it, and it is not a fault. Quiet, but present. */
.excl-line.is-impossible .excl-kind { color: var(--text-dim); }

/* Blocked: waiting on a decision or a confirmation. Someone can act. */
.excl-line.is-blocked .excl-kind {
  color: var(--amber);
  border-color: rgba(240, 180, 41, 0.34);
}

/* Failed: it was attempted and did not work. Might be retried. */
.excl-line.is-failed .excl-kind {
  color: var(--red);
  border-color: rgba(234, 95, 90, 0.34);
}

.excl-line.is-skipped .excl-kind { opacity: 0.7; }

/* Text carried for a screen reader but not drawn. Used where a mark stands in
   for words — the meaning has to reach someone who cannot see the logo. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* A project's package as two marks and an arrow. Logos rather than words
   because this sits in dense lists, where "Google Workspace to Microsoft 365"
   is longer than the row it is in. */
.pkg-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 7px;
  border: 1px solid var(--border-soft);
  border-radius: 999px;
  background: var(--bg-grain);
  white-space: nowrap;
}

.pkg-badge .pl-mark { width: 14px; height: 14px; }

.pkg-badge-arrow {
  color: var(--text-dim);
  font-size: 11px;
  line-height: 1;
}

/* -- customer migrations, source beside target ---------------------------
   One card per migration rather than one row per tenant. The flat table this
   replaces listed each tenant once per consent, so ten engagements became
   twenty-odd rows in which "Acme" and "Acme (target)" had to be paired by eye
   — while the question a consultant came to answer, whether that migration is
   ready to run, was stated nowhere. */

.mig-list { display: grid; }

.mig {
  padding: 16px 20px 18px;
  border-bottom: 1px solid var(--border-soft);
}

.mig:last-child { border-bottom: 0; }

.mig-head {
  display: flex;
  align-items: center;
  gap: 9px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}

.mig-customer {
  font-size: 13.5px;
  color: var(--text);
}

.mig-project { margin-left: auto; color: var(--text-dim); }
.mig-project:hover { color: var(--accent); }

/* Source, arrow, target. Collapses to a stack on a narrow screen, where the
   arrow would otherwise point at nothing. */
.mig-pair {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: 14px;
  align-items: start;
}

.mig-side {
  display: grid;
  gap: 6px;
  align-content: start;
  padding: 12px 14px;
  border: 1px solid var(--border-soft);
  border-radius: 8px;
  background: var(--bg-grain);
  min-width: 0;
}

.mig-side-label {
  text-transform: uppercase;
  letter-spacing: 0.07em;
  font-size: 10px;
}

.mig-tenant {
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 0;
}

.mig-tenant-text { min-width: 0; }

.mig-name {
  font-size: 12.5px;
  color: var(--text);
  overflow-wrap: anywhere;
}

.mig-domain { overflow-wrap: anywhere; }

.mig-detail { line-height: 1.45; }

.mig-arrow {
  align-self: center;
  color: var(--text-dim);
  font-size: 17px;
}

.mig-empty {
  display: grid;
  gap: 4px;
  padding: 6px 0;
}

/* A migration whose two ends are both connected is the state a consultant is
   scanning for, so it is the one the eye should find first. */
.mig.is-ready .mig-side { border-color: var(--accent-dim); }

@media (max-width: 760px) {
  .mig-pair { grid-template-columns: 1fr; }
  .mig-arrow { transform: rotate(90deg); justify-self: start; }
}

/* -- the migration package -----------------------------------------------
   What is moving, and into what. Cards rather than a dropdown because the
   answer is a shape — two platforms and an arrow — and a consultant reads
   that faster than they read three lines of similar text. */

.pkg-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 12px;
}

.pkg { display: block; cursor: pointer; }

/* The radio is offscreen but still the thing being checked, so `:checked`
   drives the card's appearance. The first version set the highlight from the
   server, which meant clicking a card did nothing visible until the form was
   saved — a control that looks broken rather than deferred. */
.pkg input[type="radio"] {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.pkg-body {
  display: grid;
  gap: 7px;
  height: 100%;
  padding: 15px 16px 16px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--bg-grain);
  transition: border-color 0.12s, background 0.12s, box-shadow 0.12s;
}

.pkg:hover .pkg-body { border-color: var(--accent-dim); }

.pkg input:checked + .pkg-body {
  border-color: var(--accent);
  background: var(--accent-wash);
  box-shadow: inset 0 0 0 1px var(--accent);
}

/* Keyboard users get the same signal: the label carries the ring because the
   input itself is offscreen. */
.pkg input:focus-visible + .pkg-body {
  outline: 2px solid var(--accent-dim);
  outline-offset: 2px;
}

/* Shown, not hidden: a consultant who reads "not built yet" knows where the
   product is going. One who sees nothing wonders whether it does this at all. */
.pkg.is-off { cursor: not-allowed; }
.pkg.is-off .pkg-body { opacity: 0.5; }
.pkg.is-off:hover .pkg-body { border-color: var(--border); }

.pkg-flow {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-bottom: 2px;
}

.pl-mark { width: 20px; height: 20px; flex: none; }
.pkg-arrow { color: var(--text-dim); font-size: 15px; }

.pkg-title {
  font-size: 13px;
  color: var(--text);
  line-height: 1.35;
}

.pkg-summary {
  font-size: 11px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--accent);
}

.pkg.is-off .pkg-summary { color: var(--text-dim); }

.pkg-detail {
  font-size: 11.5px;
  color: var(--text-muted);
  line-height: 1.5;
}

/* -- verification: sections ----------------------------------------------
   Four questions, one page, and a reader arrives with one of them. Driven by
   radio inputs and a sibling selector rather than script, so the panes survive
   printing and anything that strips JavaScript. */

.vtabs > input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }

.vtab-bar {
  display: flex;
  gap: 2px;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--border);
  margin-bottom: 20px;
}

.vtab-bar label {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 15px;
  font-size: 12.5px;
  color: var(--text-muted);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  user-select: none;
  white-space: nowrap;
}

.vtab-bar label:hover { color: var(--text); }

.vtab-count {
  padding: 0 6px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg-grain);
  font-family: var(--font-mono);
  font-size: 10.5px;
  line-height: 16px;
  color: var(--text-dim);
}

.vtab-count.ok    { color: var(--accent); border-color: var(--accent-dim); }
.vtab-count.amber { color: var(--amber); border-color: rgba(240, 180, 41, 0.4); }
.vtab-count.bad   { color: var(--red); border-color: rgba(234, 95, 90, 0.4); }

.vtab-pane { display: none; }

/* Each radio selects its own pane. Written out rather than generated because
   there are four of them and the mapping should be readable. */
#vtab-moved:checked    ~ #pane-moved,
#vtab-licences:checked ~ #pane-licences,
#vtab-gaps:checked     ~ #pane-gaps { display: block; }

#vtab-moved:checked    ~ .vtab-bar label[for="vtab-moved"],
#vtab-licences:checked ~ .vtab-bar label[for="vtab-licences"],
#vtab-gaps:checked     ~ .vtab-bar label[for="vtab-gaps"] {
  color: var(--text);
  border-bottom-color: var(--accent);
}

/* Keyboard focus has to be visible: the real input is offscreen, so the label
   carries the ring. */
.vtabs > input[type="radio"]:focus-visible ~ .vtab-bar label[for] {
  outline: 2px solid var(--accent-dim);
  outline-offset: -2px;
}

/* Printing has no tabs — show everything, in order. */
@media print {
  .vtab-bar { display: none; }
  .vtab-pane { display: block !important; }
}

/* Licence changes between tenants: who is losing what. */
.lic-delta {
  list-style: none;
  margin: 6px 0 10px;
  padding: 0;
  display: grid;
  gap: 4px;
}

.lic-delta li {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 12px;
}

.ld-name { color: var(--text); min-width: 150px; }

.ld-change {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  flex-wrap: wrap;
  min-width: 0;
}

.lic-delta .pill { margin-left: auto; flex: none; }

/* -- verification: freshness and failing checks --------------------------
   This page's failure mode was staleness — it reported checks that had been
   fixed weeks earlier, because verification records are append-only and the
   query read the whole history as current state. So the freshness strip is
   load-bearing rather than decorative: a number with no "as at" is a number
   the reader has to go and confirm in the tenant by hand. */

.verify-freshness {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding: 9px 14px;
  border: 1px solid var(--border-soft);
  border-radius: 8px;
  background: var(--bg-grain);
}

.vf-left {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--text-muted);
}

.vf-left svg { flex: none; opacity: 0.75; }
.vf-left strong { color: var(--text); font-weight: 500; }

/* Failing checks, grouped by the object they belong to. A flat list repeats
   the same site once per check and leaves the reader to reassemble it. */
.vfail-list { display: grid; }

.vfail {
  padding: 13px 20px;
  border-bottom: 1px solid var(--border-soft);
}

.vfail:last-child { border-bottom: 0; }

.vfail-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 7px;
}

.vfail-subject {
  font-size: 12.5px;
  color: var(--text);
  min-width: 0;
  overflow-wrap: anywhere;
}

.vfail-head .pill { margin-left: auto; flex: none; }

.vfail-checks {
  width: 100%;
  border-collapse: collapse;
  margin-left: 23px;
  width: calc(100% - 23px);
}

.vfail-checks td {
  padding: 3px 0;
  font-size: 12px;
  vertical-align: baseline;
}

.vfc-kind {
  color: var(--text-muted);
  width: 150px;
  padding-right: 12px !important;
}

/* Source and target side by side: "it did not match" is not actionable,
   "20 → 19" is. */
.vfc-cmp { white-space: nowrap; }
.vfc-arrow { color: var(--text-dim); margin: 0 7px; }
.vfc-when { text-align: right; white-space: nowrap; }

@media (max-width: 720px) {
  .vfail-checks, .vfail-checks tbody, .vfail-checks tr, .vfail-checks td {
    display: block;
    width: auto;
  }
  .vfail-checks tr { margin-bottom: 8px; }
  .vfc-when { text-align: left; }
}

/* Limits that apply across the migration rather than to one object. Set apart
   from the per-object list because they are context for reading it, not
   findings within it — and because repeating them per object was what made
   this section unreadable. */
.ulimits {
  padding: 14px 20px 15px;
  border-bottom: 1px solid var(--border-soft);
  background: var(--bg-grain);
}

.ulimits-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 9px;
}

.ulimits-title {
  font-size: 11px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.ulimit-list {
  list-style: none;
  display: grid;
  gap: 6px;
  margin: 0;
  padding: 0;
}

.ulimit {
  display: flex;
  gap: 10px;
  align-items: baseline;
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.5;
}

/* The count is the part that replaces the repetition: one line, and how many
   objects it covers. */
.ulimit-count {
  flex: none;
  min-width: 30px;
  padding: 1px 6px;
  border-radius: 4px;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: 11px;
  text-align: center;
}

/* -- the wavefield -------------------------------------------------------
   Curved channels carrying each workload from the source tenant to the
   target. Answers "is anything flowing?" at a glance; the lanes underneath
   answer "how much, exactly?".

   The crest is a dashed stroke whose dash length is the percentage verified,
   so the filled part of each curve is literally how far that workload has
   got. Nothing here is decorative — stroke weight is share of the estate. */

.wavefield {
  display: grid;
  grid-template-columns: minmax(96px, auto) 1fr minmax(96px, auto);
  gap: 14px;
  align-items: center;
  padding: 4px 0 18px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border-soft);
}

.wf-svg {
  width: 100%;
  height: var(--wave-h, 240px);
  display: block;
  overflow: visible;
}

.wf-end {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel-raised);
  min-width: 0;
}

.wf-end.is-target {
  border-color: rgba(51, 210, 150, 0.34);
  background: var(--accent-wash);
  position: relative;
  isolation: isolate;
  transition: border-color 420ms ease, box-shadow 420ms ease;
}

/* Arrivals landing on the target.

   Each wave carries a different workload colour, so the glow is a blend of
   them rather than one hue: what reaches the target is mail *and* files *and*
   Teams, and a single colour would imply a single stream. The blend sits
   behind the card and pulses while anything is in flight.

   Purely decorative, so it is drawn on a pseudo-element rather than the card
   itself — nothing here should be able to shift the text it sits behind. */
.wf-end.is-target::before {
  content: "";
  position: absolute;
  inset: -14px;
  z-index: -1;
  border-radius: calc(var(--radius) + 10px);
  opacity: 0;
  transition: opacity 600ms ease;
  background:
    radial-gradient(58% 74% at 4% 32%, rgba(75, 163, 240, 0.50), transparent 68%),
    radial-gradient(54% 66% at 2% 62%, rgba(23, 176, 181, 0.46), transparent 66%),
    radial-gradient(48% 58% at 8% 88%, rgba(166, 173, 255, 0.42), transparent 64%),
    radial-gradient(46% 54% at 6% 10%, rgba(43, 136, 216, 0.40), transparent 62%);
  filter: blur(9px);
}

/* While anything is moving, the blend breathes — the card is being hit. */
.wavefield.is-moving .wf-end.is-target::before {
  opacity: 1;
  animation: wf-impact 2.6s ease-in-out infinite;
}

.wavefield.is-moving .wf-end.is-target {
  border-color: rgba(75, 163, 240, 0.52);
  box-shadow: 0 0 0 1px rgba(75, 163, 240, 0.14),
              0 6px 26px -8px rgba(58, 155, 224, 0.42);
}

@keyframes wf-impact {
  0%, 100% { opacity: 0.55; transform: scale(0.985); }
  50%      { opacity: 1;    transform: scale(1.015); }
}

/* Finished. A different colour on purpose: the same green the console uses
   for a verified object everywhere else, so "arrived" reads the same here as
   it does on a ledger row. It holds steady rather than pulsing — the work is
   over, and a thing that keeps moving reads as a thing still running. */
.wavefield.is-complete .wf-end.is-target::before {
  opacity: 1;
  animation: none;
  filter: blur(11px);
  background:
    radial-gradient(64% 78% at 6% 50%, rgba(51, 210, 150, 0.46), transparent 70%),
    radial-gradient(48% 58% at 12% 18%, rgba(51, 210, 150, 0.26), transparent 64%),
    radial-gradient(48% 58% at 12% 82%, rgba(51, 210, 150, 0.26), transparent 64%);
}

.wavefield.is-complete .wf-end.is-target {
  border-color: rgba(51, 210, 150, 0.62);
  box-shadow: 0 0 0 1px rgba(51, 210, 150, 0.18),
              0 6px 24px -10px rgba(51, 210, 150, 0.40);
}

.wavefield.is-complete .wf-end.is-target .wf-end-label::after {
  content: " · migrated";
  color: var(--accent);
  letter-spacing: 0.04em;
}

/* A migration is watched for hours. Anyone who has asked the system to calm
   down gets the state without the movement. */
@media (prefers-reduced-motion: reduce) {
  .wavefield.is-moving .wf-end.is-target::before { animation: none; }
}

.wf-end-label {
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.wf-end.is-target .wf-end-label { color: var(--accent); }

.wf-domain {
  font-size: 11px;
  color: var(--text);
  word-break: break-all;
  line-height: 1.35;
}

/* Each wave inherits its workload colour through `currentColor`, so the
   channel and crest never drift apart. */
.wf-wave { color: var(--tone-colour, var(--text-dim)); }

.wf-channel {
  stroke: currentColor;
  opacity: 0.12;
}

.wf-crest {
  stroke: currentColor;
  opacity: 0.9;
  transition: stroke-dasharray 0.8s ease;
  filter: drop-shadow(0 0 3px currentColor);
}

/* Carrying bytes right now: the crest surges along its own length and a
   packet rides it. Dashes drift toward the target so the direction of travel
   is visible without reading a word. */
.wf-wave.is-moving .wf-crest {
  animation: wf-breathe 2s ease-in-out infinite;
}

.wf-wave.is-moving .wf-channel {
  stroke-dasharray: 5 9;
  animation: wf-drift 1.1s linear infinite;
  opacity: 0.34;
}

@keyframes wf-breathe {
  50% { opacity: 0.55; }
}

@keyframes wf-drift {
  to { stroke-dashoffset: -14; }
}

/* The whole field, while any workload is in flight. A migration that is
   running should look different from one that is finished, at a glance and
   from across a room. */
.flow-panel.is-flowing .wf-svg { animation: wf-alive 3.6s ease-in-out infinite; }

@keyframes wf-alive {
  50% { filter: brightness(1.09); }
}

.wf-packet {
  fill: currentColor;
  filter: drop-shadow(0 0 5px currentColor);
}

/* A workload nobody has queued is a quieter thread — present and legible,
   but clearly not part of the current. Dimming the whole group any further
   loses the darker brand colours against the background entirely. */
.wf-wave.is-idle { opacity: 0.72; }
.wf-wave.is-idle .wf-channel { opacity: 0.2; }

@media (prefers-reduced-motion: reduce) {
  .wf-wave.is-moving .wf-crest { animation: none; }
  .wf-packet { display: none; }
}

@media (max-width: 760px) {
  /* The left-to-right metaphor needs width. Below this the ends stack and the
     waves become a shorter band rather than an unreadable squeeze. */
  .wavefield { grid-template-columns: 1fr; }
  .wf-svg { height: 150px; }
}


/* Hover cards. The detail belongs to the curve you are pointing at, rather
   than to a second list of the same seven workloads underneath. */

.wavefield { position: relative; }

/* A wide invisible stroke, so a 2px curve is actually hoverable. Without it
   the cards exist but nobody can open them. */
.wf-hit {
  stroke: transparent;
  cursor: pointer;
  pointer-events: stroke;
}

.wf-wave { outline: none; }

.wf-card {
  position: absolute;
  left: 0;
  top: 0;
  transform: translateY(-4px);
  z-index: 5;
  width: 216px;
  padding: 11px 13px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--tone-colour, var(--accent));
  border-radius: 9px;
  background: var(--panel-raised);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.5);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
}

/* Opened by script, because the card follows the pointer — a fixed position
   puts it where the layout decided rather than where the eye is. */
.wf-card.is-open {
  opacity: 1;
  visibility: visible;
  transform: none;
}

/* The hovered wave brightens; the rest recede, so the curve and its card are
   unmistakably the same thing. */
.wavefield:has(.wf-wave:hover) .wf-wave:not(:hover) { opacity: 0.28; }
.wf-wave:hover .wf-crest { opacity: 1; }
.wf-wave:hover .wf-channel { opacity: 0.3; }
.wf-wave:focus-visible .wf-crest { opacity: 1; }

.wf-card-head {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-bottom: 8px;
}

.wf-card-icon { display: inline-flex; }
.wf-card-icon .wl-icon { width: 15px; height: 15px; color: var(--tone-colour); }
.wf-card-name { font-size: 12.5px; color: var(--text); }
.wf-card-pct { margin-left: auto; font-size: 12px; color: var(--tone-colour); }

.wf-card-stats {
  display: grid;
  gap: 3px;
  font-size: 11.5px;
  line-height: 1.45;
}

@media (hover: none) {
  /* No hover on touch: the cards stack under the waves instead of hiding
     information behind a gesture that does not exist. */
  .wf-card {
    position: static;
    transform: none;
    opacity: 1;
    visibility: visible;
    width: auto;
    margin-top: 8px;
    box-shadow: none;
  }
}


/* The travelling knot. One leaves the source, crosses, the next follows —
   staggered per lane in `_add_wave_geometry`, because seven knots leaving
   together read as one pulsing bar rather than seven channels. */

.wf-knot { color: inherit; }

.wf-knot-core {
  fill: currentColor;
  filter: drop-shadow(0 0 6px currentColor);
}

/* A soft halo so the knot reads as carrying something, not as a dot. */
.wf-knot-halo {
  fill: currentColor;
  opacity: 0.18;
}

/* On a lane actively copying, the knot is brighter and pulses as it travels. */
.wf-wave.is-moving .wf-knot-core { animation: knot-pulse 1.2s ease-in-out infinite; }
.wf-wave.is-moving .wf-knot-halo { opacity: 0.3; }

@keyframes knot-pulse {
  50% { r: 4.2; }
}

/* An idle lane still shows a knot, but a quiet one: the channel exists and is
   ready, which is different from nothing being there at all. */
.wf-wave.is-idle .wf-knot-halo { opacity: 0.08; }

@media (prefers-reduced-motion: reduce) {
  .wf-knot { display: none; }
}

/* -- cutover waves -------------------------------------------------------
   The schedule behind the flow. A wave reports what the ledger says about it
   rather than only the name someone typed, because "Wave 2 — Finance" tells a
   consultant nothing about whether Finance has actually landed. */

.wave-list { display: grid; }

.wave {
  display: grid;
  grid-template-columns: 46px 1fr;
  gap: 12px;
  padding: 13px 20px;
  border-bottom: 1px solid var(--border-soft);
  align-items: start;
}

.wave:last-child { border-bottom: 0; }

/* Work in no wave at all — shown, not hidden. Unplanned work is exactly what
   a consultant needs to notice before a cutover date is agreed. */
.wave.is-loose { background: rgba(240, 180, 41, 0.04); }

.wave-seq {
  font-size: 12px;
  color: var(--text-dim);
  padding-top: 2px;
  text-align: right;
}

.wave-top {
  display: flex;
  align-items: center;
  gap: 9px;
  flex-wrap: wrap;
  margin-bottom: 7px;
}

.wave-name { font-size: 13px; color: var(--text); }

.wave-track {
  height: 4px;
  border-radius: 3px;
  background: var(--bg-grain);
  overflow: hidden;
}

.wave-fill {
  height: 100%;
  border-radius: 3px;
  background: var(--accent-dim);
  transition: width 0.7s ease;
}

.wave-fill.is-live {
  background: linear-gradient(90deg, var(--accent-dim), var(--accent), var(--accent-dim));
  background-size: 200% 100%;
  animation: prog-sheen 1.4s linear infinite;
}

.wave-fill.is-bad { background: var(--red); }

.wave-foot { display: flex; gap: 5px; margin-top: 6px; align-items: baseline; }

/* Wave statuses, plus the synthetic "unscheduled" bucket. */
.pill.planned     { color: var(--text-muted); }
.pill.ready       { color: var(--accent); }
.pill.running     { color: var(--accent-bright); }
.pill.cutover     { color: var(--amber); }
.pill.complete    { color: var(--accent); }
.pill.rolled_back { color: var(--amber); }
.pill.failed      { color: var(--red); }
.pill.unscheduled { color: var(--amber); }

/* -- migration flow ------------------------------------------------------
   The overview's centrepiece: what is crossing from the source tenant to the
   target, by workload. Read from the ledger — nothing here is typed in.

   Each lane carries its own colour, taken from the Microsoft product mark it
   represents. A mailbox stalled on licence propagation and a SharePoint
   library mid-copy should be distinguishable at a glance, without reading. */

.flow-panel .panel-body { padding: 18px 20px 20px; }

.flow {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 18px;
  align-items: center;
}

@media (max-width: 900px) {
  /* Stacked: the arrow metaphor breaks below this width, so the ends become
     labels above and below the lanes rather than a squeezed three-column. */
  .flow { grid-template-columns: 1fr; gap: 12px; }
}

.flow-end { text-align: center; min-width: 116px; }

.flow-end-label {
  font-size: 10.5px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-bottom: 7px;
}

.flow-tenant {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 11px 13px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel-raised);
  font-size: 12px;
  color: var(--text);
  word-break: break-all;
}

.flow-tenant.is-target {
  border-color: rgba(51, 210, 150, 0.34);
  background: var(--accent-wash);
  color: var(--accent);
}

.flow-end-sub { margin-top: 6px; }

.flow-lanes { display: grid; gap: 9px; min-width: 0; }

.lane {
  padding: 9px 12px;
  border: 1px solid var(--border-soft);
  border-left: 3px solid var(--lane-colour, var(--border));
  border-radius: 8px;
  background: var(--panel-raised);
}

.lane-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 7px;
}

.lane-icon { display: inline-flex; flex: none; }
.lane-icon .wl-icon { width: 15px; height: 15px; color: var(--lane-colour); }

.lane-label { font-size: 12.5px; color: var(--text); }
.lane-count { margin-left: auto; font-size: 11.5px; }

.lane-track {
  position: relative;
  height: 5px;
  border-radius: 3px;
  background: var(--bg-grain);
  overflow: hidden;
}

.lane-fill {
  height: 100%;
  border-radius: 3px;
  background: var(--lane-colour, var(--accent-dim));
  transition: width 0.7s ease;
}

/* A sweep along the track while bytes are actually crossing. */
.lane-pulse {
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.28), transparent);
  animation: lane-sweep 1.6s linear infinite;
}

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

.lane-foot {
  display: flex;
  gap: 6px;
  margin-top: 6px;
  align-items: baseline;
}

/* Per-workload colours, taken from the product marks and lifted where the
   brand value is too dark to read as a 2px line on this background. The point
   is recognition — Outlook blue, SharePoint teal — not exact brand values on a
   surface those values were never chosen for. */
.lane[class*="is-"] { --lane-colour: var(--text-dim); }
.flow-lanes .lane { --lane-colour: var(--tone-colour, var(--text-dim)); }

.lane-connection { --tone-colour: #7A8B99; }
.lane-identity   { --tone-colour: #2B88D8; }  /* Entra blue */
.lane-licence    { --tone-colour: #C98BDB; }  /* distinct from every product */
.lane-mail       { --tone-colour: #4BA3F0; }  /* Outlook blue, lifted for a thin stroke */
.lane-onedrive   { --tone-colour: #3A9BE0; }  /* OneDrive blue, lifted for a thin stroke */
.lane-sharepoint { --tone-colour: #17B0B5; }  /* SharePoint teal, lifted for a thin stroke */
.lane-teams      { --tone-colour: #A6ADFF; }  /* Teams purple, lifted to read on the dark ground */

/* State overrides the product colour only where it must: a failure is red
   whatever workload it is in, because that is the thing to look at first. */
.lane.is-attention { --tone-colour: var(--red); }
.lane.is-idle      { --tone-colour: var(--border); opacity: 0.62; }
/* Discovered but waiting on a person to choose a destination. Amber, because
   the migration is blocked on a decision rather than on the machine. */
.lane.is-undecided { --tone-colour: var(--amber); }

.flow-live {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 11.5px;
  color: var(--accent);
}

@media (prefers-reduced-motion: reduce) {
  .lane-pulse { animation: none; }
}

/* -- workload ticks -------------------------------------------------------
   The ledger is a grid of subjects against workloads. A tick means verified —
   moved and proven. Nothing else earns one, because a written-but-unchecked
   object is not complete (BUILD_PLAN §8.8), so it shows a warning instead. */

.wl-th {
  text-align: center;
  width: 78px;
  white-space: nowrap;
}

.wl-td { text-align: center; }

.wl-cell {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1px solid transparent;
  line-height: 1;
}

.wl-cell.is-ok {
  color: var(--accent);
  background: rgba(51, 210, 150, 0.12);
  border-color: rgba(51, 210, 150, 0.32);
}

.wl-cell.is-bad {
  color: var(--red);
  background: var(--red-wash);
  border-color: rgba(234, 95, 90, 0.34);
}

.wl-cell.is-warn {
  color: var(--amber);
  background: rgba(240, 180, 41, 0.12);
  border-color: rgba(240, 180, 41, 0.32);
}

/* Waiting on the target tenant to finish a dependency. Deliberately not red:
   nothing is wrong, and colouring a normal wait as a failure hides the rows
   that genuinely need attention. */
.wl-cell.is-wait {
  color: var(--amber);
  background: rgba(240, 180, 41, 0.10);
  border-color: rgba(240, 180, 41, 0.28);
}

.wl-cell.is-busy {
  color: var(--accent);
  background: rgba(51, 210, 150, 0.08);
  border-color: rgba(51, 210, 150, 0.22);
  animation: wl-spin 1.1s linear infinite;
}

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

@media (prefers-reduced-motion: reduce) {
  .wl-cell.is-busy { animation: none; }
}

.wl-cell.is-queued {
  color: var(--text-dim);
  border-color: var(--border);
}

/* Not part of this migration at all — present, but visually quiet, so the
   eye is drawn to the states that need a decision. */
.wl-cell.is-none {
  color: var(--text-dim);
  opacity: 0.42;
}

.ledger-detail { max-width: 340px; }

@media print {
  .sidebar, .topbar, .toasts, .form-actions { display: none !important; }
  .main { margin-left: 0; }
  html, body { background: #fff; color: #000; }
}


/* -- mobile: a tab bar, not a drawer -------------------------------------
   Below 940px the sidebar becomes a slide-in drawer, which is a desktop
   pattern shrunk rather than a mobile one. A phone user reaches the bottom of
   the screen with a thumb and the top-left corner with neither hand, so the
   primary destinations move to a fixed bottom bar and the rest go behind
   "More".

   Four tabs plus More, deliberately: five targets is what fits at a tappable
   44px on a 360px screen, and a bar that scrolls sideways is one where the
   last item is never found. */

.tabbar { display: none; }
.more-sheet { display: none; }

@media (max-width: 940px) {
  .tabbar {
    display: flex;
    position: fixed;
    inset: auto 0 0 0;
    z-index: 45;
    background: var(--panel);
    border-top: 1px solid var(--border);
    /* Clears the iOS home indicator. Without it the last row of labels sits
       under the gesture bar and the taps land on the wrong thing. */
    padding-bottom: env(safe-area-inset-bottom, 0);
  }

  .tab {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    /* 44px of height before padding: the smallest target a thumb hits
       reliably, and the figure both platform guidelines settle on. */
    padding: 9px 4px 8px;
    min-height: 52px;
    color: var(--text-dim);
    font-size: 10.5px;
    font-weight: 550;
    letter-spacing: 0.01em;
    background: none;
    border: 0;
    cursor: pointer;
    transition: color 130ms var(--ease);
    -webkit-tap-highlight-color: transparent;
  }

  .tab svg { flex: 0 0 auto; }
  .tab:hover { color: var(--text-muted); }
  .tab.is-active { color: var(--accent); }

  /* The active mark sits on top of the bar rather than under the label, so a
     tab reads as selected even when the label is clipped. */
  .tab.is-active::before {
    content: "";
    position: absolute;
    top: 0;
    width: 26px;
    height: 2px;
    border-radius: 0 0 2px 2px;
    background: var(--accent);
  }
  .tab { position: relative; }

  .tab-label {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* The drawer and its trigger are replaced entirely, not merely hidden:
     leaving both interaction models live is how a user finds two different
     menus that disagree about what is selected. */
  .sidebar { display: none; }

  .main { margin-left: 0; }
  /* Room for the bar, so the last row of a table is not sitting under it. */
  .content { padding: 18px 18px calc(76px + env(safe-area-inset-bottom, 0)); }
  .topbar { padding: 0 18px; }

  /* ---- the More sheet ---- */
  .more-sheet {
    display: block;
    position: fixed;
    inset: auto 0 0 0;
    z-index: 55;
    background: var(--panel);
    border-top: 1px solid var(--border);
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    box-shadow: var(--shadow-pop);
    padding: 8px 16px calc(20px + env(safe-area-inset-bottom, 0));
    max-height: 80vh;
    overflow-y: auto;
    /* Off-screen rather than display:none, so it slides rather than appears. */
    transform: translateY(100%);
    transition: transform 260ms var(--ease);
    visibility: hidden;
  }
  .more-sheet.is-open { transform: translateY(0); visibility: visible; }

  /* The grab handle. Not interactive — it is the affordance that says this
     panel came from the bottom and goes back there. */
  .sheet-grip {
    width: 36px;
    height: 4px;
    border-radius: 999px;
    background: var(--border);
    margin: 4px auto 14px;
  }

  .sheet-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
  }
  .sheet-title { font-size: 15px; font-weight: 640; color: var(--text); }

  .sheet-section {
    font-size: 10.5px;
    font-weight: 640;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--text-dim);
    margin: 16px 0 8px;
  }
  .sheet-section:first-of-type { margin-top: 4px; }

  /* Cards rather than a list: a list in a sheet is the drawer again, and the
     point of the sheet is that a destination is a target rather than a line. */
  .sheet-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
  }

  .sheet-card {
    display: flex;
    flex-direction: column;
    gap: 9px;
    padding: 14px;
    border-radius: var(--radius);
    border: 1px solid var(--border-soft);
    background: var(--bg-grain);
    color: var(--text-muted);
    font-size: 13px;
    font-weight: 550;
    min-height: 76px;
    transition: border-color 130ms var(--ease), background 130ms var(--ease);
    -webkit-tap-highlight-color: transparent;
  }
  .sheet-card:active { background: var(--panel-raised); }
  .sheet-card.is-active {
    border-color: var(--accent-dim);
    background: var(--accent-wash);
    color: var(--accent);
  }
  .sheet-card svg { color: var(--text-dim); }
  .sheet-card.is-active svg { color: var(--accent); }

  .sheet-scrim {
    position: fixed;
    inset: 0;
    z-index: 50;
    background: rgba(0, 0, 0, 0.55);
    opacity: 0;
    visibility: hidden;
    transition: opacity 220ms var(--ease), visibility 220ms var(--ease);
  }
  .sheet-scrim.is-open { opacity: 1; visibility: visible; }

  /* The signed-in row, at the foot of the sheet where the sidebar kept it. */
  .sheet-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-top: 18px;
    padding-top: 14px;
    border-top: 1px solid var(--border-soft);
  }
}

@media (prefers-reduced-motion: reduce) {
  .more-sheet, .sheet-scrim { transition: none; }
}

/* Vendor marks.

   The lettermark is a tile in the vendor's own brand colour; the real logo is
   never given one, because placing a partner-kit wordmark on a coloured
   background is the thing every brand guideline forbids. */
.vendor-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  border-radius: 7px;
  background: var(--vendor, #6B7280);
  color: #fff;
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
  /* A dark wordmark on its own dark brand colour still needs an edge against
     a dark page, and a light one needs the same against a light page. */
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.12);
}

/* A vendor logo, never restyled.

   The console is dark (--bg: #0C0F0E), and brand kits ship two variants for
   exactly that reason: a mark drawn for light backgrounds and one drawn for
   dark. Sophos's 2025 wordmark is #001A47 navy and disappears here;
   CrowdStrike's dark-background variant is near-white and would disappear on
   a light page.

   The answer is the one the kits themselves prescribe — give a light-variant
   mark its own white plate. That is clear space, which every guideline
   requires anyway; it is not recolouring, cropping or restyling, all of which
   they forbid. `needs-plate` marks the logos that require it, so a logo
   already drawn for dark backgrounds sits directly on the page as intended. */
.vendor-logo {
  flex: 0 0 auto;
  width: auto;
  height: 22px;
  max-width: 96px;
  /* Clear space, per every partner brand kit. */
  padding: 4px 2px;
  object-fit: contain;
}

.vendor-logo.needs-plate {
  background: #fff;
  border-radius: 5px;
  padding: 5px 8px;
  height: 32px;
  max-width: 116px;
  box-sizing: border-box;
}

.vendor-cell {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

.vendor-cell > span:last-child { min-width: 0; }

/* Wraps to one column on a phone rather than overflowing the panel. */
.vendor-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 12px 18px;
}

/* Vendor tiles and the panel they open.

   A vendor is a button rather than a link: clicking it changes what is on the
   page instead of navigating, and a screen reader should be told which. */
.vendor-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 10px;
}

.vendor-tile {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 13px;
  text-align: left;
  background: var(--panel-raised);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  color: inherit;
  font: inherit;
  cursor: pointer;
  transition: border-color .12s ease, background .12s ease;
}

.vendor-tile:hover { border-color: var(--border); background: var(--panel); }
.vendor-tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* A connected vendor carries the accent on its edge — the one thing worth
   distinguishing at a glance on this grid. */
.vendor-tile.is-watched { border-color: var(--accent-dim); }

.vendor-tile-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.vendor-tile-name { font-size: 13px; }
.vendor-tile-note {
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vendor-panel {
  margin-top: 20px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.vendor-panel-head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--border-soft);
  flex-wrap: wrap;
}

.vendor-panel-title { font-size: 14px; }
.vendor-panel-body { padding: 16px; }

/* One domain, ticked or explained. A disabled option always says why next to
   it — a greyed-out checkbox with no reason is a question the page provoked
   and refused to answer. */
.domain-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 8px 16px;
}

.domain-option {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  font-size: 12.5px;
  line-height: 1.45;
  cursor: pointer;
}

.domain-option input { margin-top: 2px; flex: 0 0 auto; }
.domain-option.is-taken,
.domain-option.is-unavailable { color: var(--text-dim); cursor: default; }

.cred-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 12px;
}

.field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.field-label {
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--text-muted);
  letter-spacing: 0.02em;
}
.field input { width: 100%; }

/* Coverage — one row per part of the estate, edited in place.
   The controls sit inside a table cell, so they are sized to the row rather
   than to a form: a full-width select here would push the table sideways on
   every screen narrower than a desk monitor. */
.coverage-form { gap: 8px; }
.coverage-pick, .coverage-note {
  padding: 5px 9px;
  font-size: 12.5px;
  height: auto;
}
.coverage-pick { min-width: 190px; }
.coverage-note { min-width: 150px; flex: 1 1 150px; }

@media (max-width: 900px) {
  .coverage-pick, .coverage-note { min-width: 0; width: 100%; }
}

@media print {
  .tabbar, .more-sheet, .sheet-scrim { display: none !important; }
}

/* ---------- lane detail ----------
   What is moving inside one workload. The flow curve answers "how far along";
   this answers "what is it doing" — the question a single 279 GB archive makes
   unanswerable in a percentage. */

.wf-card-more {
  display: block;
  width: 100%;
  margin: .5rem 0 .1rem;
  padding: .32rem .5rem;
  font: inherit;
  font-size: .78rem;
  color: var(--accent);
  background: var(--accent-wash);
  border: 1px solid var(--accent-dim);
  border-radius: 7px;
  cursor: pointer;
  text-align: center;
}
.wf-card-more:hover { background: var(--panel-raised); }

.lane-dialog {
  width: min(760px, 92vw);
  max-height: 84vh;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--panel);
  color: var(--text);
  overflow: auto;
}
.lane-dialog::backdrop { background: rgba(0, 0, 0, .58); }
.lane-dialog-close { position: sticky; top: 0; text-align: right; z-index: 2; }
.lane-dialog-close button {
  margin: .4rem .5rem 0;
  width: 30px; height: 30px;
  font-size: 1.15rem; line-height: 1;
  color: var(--text-muted);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 7px;
  cursor: pointer;
}

.lane-detail { padding: 0 1.15rem 1.15rem; }
.lane-detail-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; margin-bottom: .8rem;
}

.lane-rows { display: grid; gap: .6rem; }
.lane-row {
  padding: .7rem .8rem;
  border: 1px solid var(--line-soft);
  border-radius: 10px;
  background: var(--ink-soft);
}
/* A live row is the one worth looking at, so it gets the only accent here. */
.lane-row.is-live { border-color: var(--accent-dim); }
.lane-row.has-error { border-color: rgba(234, 95, 90, .4); }

.lane-row-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: .7rem; margin-bottom: .45rem; flex-wrap: wrap;
}
.lane-row-name { display: grid; gap: .1rem; min-width: 0; }
.lane-row-name .mono { overflow-wrap: anywhere; }

/* Deliberately thin. It is a position indicator, not a headline — the numbers
   underneath carry the actual information. */
.lane-bar {
  height: 5px;
  border-radius: 3px;
  background: var(--line-soft);
  overflow: hidden;
  margin: .3rem 0 .5rem;
}
.lane-bar span {
  display: block; height: 100%;
  background: var(--accent);
  /* Always visible once anything has moved: a hairline at 0.3% still says
     "started", where an empty track says "nothing is happening". */
  min-width: 2px;
  transition: width .3s ease;
}

.lane-row-stats {
  display: flex; flex-wrap: wrap; gap: .25rem 1.1rem;
  font-size: .82rem;
}
.lane-row-error {
  margin-top: .45rem;
  font-size: .78rem;
  color: var(--red);
  overflow-wrap: anywhere;
}

@media (max-width: 560px) {
  .lane-row-stats { gap: .2rem .7rem; font-size: .78rem; }
}

/* A lane carrying work is a button: clicking the curve opens its item list.
   The hover card is a preview, and a preview you have to chase with the
   pointer is one nobody can use. */
.wf-wave.is-clickable { cursor: pointer; }
.wf-wave.is-clickable:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.wf-wave.is-clickable:hover .wf-crest { filter: brightness(1.25); }

/* ---------- one person's migration ----------
   Opened from a name. The project flow says how the estate is progressing;
   this says what is happening to Julie, which is the question consultants are
   actually asked. */

.person-detail { padding: 0 1.15rem 1.15rem; }
.person-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; margin-bottom: .9rem;
}
.person-head-stat { text-align: right; font-size: .86rem; }

.person-rows { display: grid; gap: .55rem; }
.person-row {
  display: flex; gap: .7rem;
  padding: .7rem .8rem;
  border: 1px solid var(--line-soft);
  border-radius: 10px;
  background: var(--ink-soft);
}
/* Only a row that is genuinely moving gets the accent. A page where every
   row is highlighted has highlighted nothing. */
.person-row.is-live { border-color: var(--accent-dim); }
.person-row.has-error { border-color: rgba(234, 95, 90, .4); }

.person-icon { flex: 0 0 auto; padding-top: 2px; }
.ico-ok   { color: var(--accent); }
.ico-bad  { color: var(--red); }
.ico-warn { color: var(--amber); }
.ico-dim  { color: var(--text-dim); }
.ico-spin { color: var(--accent); animation: person-spin 1s linear infinite; }
@keyframes person-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .ico-spin { animation: none; }
}

.person-row-body { flex: 1 1 auto; min-width: 0; }
.person-row-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: .6rem; flex-wrap: wrap; margin-bottom: .35rem;
}
.person-row-name { font-weight: 500; }

.person-bar {
  height: 5px; border-radius: 3px;
  background: var(--line-soft);
  overflow: hidden; margin: .3rem 0 .45rem;
}
.person-bar span {
  display: block; height: 100%;
  background: var(--accent);
  /* Visible the moment anything has moved: a hairline says "started", an
     empty track says "nothing is happening". */
  min-width: 2px;
  transition: width .3s ease;
}

.person-row-stats {
  display: flex; flex-wrap: wrap; gap: .2rem 1rem; font-size: .82rem;
}
.person-row-error {
  margin-top: .4rem; font-size: .78rem; color: var(--red);
  overflow-wrap: anywhere;
}

@media (max-width: 560px) {
  .person-head-stat { text-align: left; }
  .person-row-stats { gap: .15rem .7rem; font-size: .78rem; }
}

/* The person's name as a control. It has to stay looking like a table cell —
   a row of buttons reads as a toolbar — so only the cursor and a hover tint
   say it is clickable. */
.person-open {
  display: block; width: 100%;
  padding: 0; border: 0; background: none;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.person-open:hover .cell-primary { color: var(--accent); }
.person-open .cell-primary,
.person-open .cell-sub { display: block; }
.person-open:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px;
}
