/* ============================================================
   De Haas Site Manager - app.css
   Operator-UI design system. Dark mode, dense, single accent.
   ============================================================ */

/* === Tokens ============================================== */
:root {
  --bg:               #0a0a0d;
  --bg-dim:           #07070a;
  --surface:          #111114;
  --surface-2:        #16161b;
  --surface-3:        #1c1c22;
  --hairline:         #1f1f26;
  --hairline-strong:  #2a2a33;
  --text:             #f5f5f7;
  --text-2:           #c4c4cc;
  --text-3:           #8a8a96;
  /* DECORATION ONLY. 3.2–3.8:1 against the surfaces in this file, so it fails
     WCAG AA for text and must never carry content — no labels, no values, no
     placeholders. Separators and chevrons only, which 1.4.3 exempts. Anything
     a reader has to read is --text-3 or lighter. Raising this to pass 4.5:1
     would put it within a hair of --text-3 and collapse the ramp, so the token
     keeps its job and gives up the misuse. */
  --text-4:           #6b6b76;
  /* Consumed by the quiet <pre> blocks in ftp/accounts/cron. It was never
     defined, so all three rendered at inherited --text — the opposite of the
     intent. Aliased rather than given its own value: one quiet grey is enough. */
  --muted:            var(--text-3);
  --accent:           #c026d3;
  --accent-2:         #d946ef;
  --accent-soft:      rgba(192, 38, 211, 0.18);
  --accent-glow:      rgba(192, 38, 211, 0.25);
  --ok:               #4ade80;
  --warn:             #fbbf24;
  --bad:              #f43f5e;
  --info:             #38bdf8;

  --sans:  "Geist", "Söhne", system-ui, -apple-system, sans-serif;
  --mono:  "Geist Mono", "JetBrains Mono", ui-monospace, monospace;
  /* --serif kept for back-compat with existing inline styles in views.
     Aliased to sans so the panel reads consistently. */
  --serif: "Geist", system-ui, sans-serif;

  --r-sm: 6px;
  --r:    8px;
  --r-md: 10px;
  --r-lg: 14px;

  /* Spacing. 4-based, deliberately few steps: rhythm comes from the contrast
     between a tight interval and a generous one, and a scale with a value
     every 2px has no contrast left to spend. Everything in this file resolves
     to one of these — if a layout wants 18px, one of 16 or 20 is the answer.
     Grouping rule: --s-2/--s-3 inside a group, --s-6 between groups. */
  --s-0:  2px;
  --s-1:  4px;
  --s-2:  8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 20px;
  --s-6: 24px;
  --s-7: 32px;
  --s-8: 48px;
  --s-9: 64px;

  /* Type. Fixed px, never fractional — a 13.5px label lands on a half pixel
     and renders soft, which reads as blur rather than as a size. Steps are
     ~1.15 apart at the top and compress toward the bottom, where letterspaced
     mono caps stay legible well below body size. */
  --t-2xs: 10px;   /* letterspaced mono caps: nav groups, stat keys, tags   */
  --t-xs:  11px;   /* field labels, table heads, row actions                */
  --t-sm:  12px;   /* metadata, table body, secondary text                  */
  --t-md:  13px;   /* the UI default: buttons, inputs, body                 */
  --t-lg:  15px;   /* card titles                                          */
  --t-xl:  17px;   /* stat values, modal + empty-state titles              */
  --t-2xl: 20px;   /* page titles                                          */

  /* One height for every control, so an input, a select and a button sitting
     in the same row line up. They used to be 36px, 42px and 34px. */
  --control-h: 38px;
}

/* === Base ================================================ */
* { box-sizing: border-box; }
html, body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: var(--t-md);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
body { min-height: 100dvh; }

::selection { background: var(--accent-soft); color: var(--text); }

button { font-family: inherit; color: inherit; }
input, textarea, select { font-family: inherit; color: inherit; }

a { color: var(--text-2); text-decoration: none; transition: color 0.12s ease; }
a:hover { color: var(--text); }

code, .mono { font-family: var(--mono); }

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

/* Visually hidden, still announced. The clip-rect technique rather than
   display:none or visibility:hidden, both of which remove the element from the
   accessibility tree — which would defeat the point. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Off-screen until focused. Uses the same accent outline as every other
   focus ring so it reads as part of the system, not a bolt-on. */
.skip-link {
  position: fixed;
  top: var(--s-3);
  left: var(--s-3);
  z-index: 40;
  transform: translateY(-200%);
  background: var(--surface-2);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r-sm);
  padding: var(--s-2) var(--s-4);
  font-size: var(--t-md);
  color: var(--text);
  transition: transform 0.12s ease;
}
.skip-link:focus { transform: translateY(0); }

/* === Shell: centered (setup, login) ====================== */
.shell-bleed {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: var(--s-8) var(--s-5) var(--s-9);
}

/* === Cards =============================================== */
.card {
  /* .card is overflow:hidden for its border radius, so anything wider than it
     is simply cut off — no scrollbar, no way to reach it. Filesystem paths,
     schema names and email addresses have no break opportunities, so at 390px
     they were being truncated mid-string. Break them instead. Set on .card
     rather than .card-body because several views put content directly in the
     card. Tables are exempt: they get a real scroller on .card-body-flush,
     and breaking a path inside a table cell hurts more than scrolling to it. */
  overflow-wrap: anywhere;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  overflow: hidden;
  position: relative;
}
.card-bleed {
  width: 100%;
  max-width: 420px;
  padding: var(--s-7);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
}

/* === Brand mark ========================================== */
.brand-mark {
  width: 40px;
  height: 40px;
  border-radius: 8px;
  background: var(--surface-2);
  border: 1px solid var(--hairline-strong);
  display: grid;
  place-items: center;
  margin: 0 auto;
  color: var(--accent);
}
.brand-mark svg { width: 20px; height: 20px; }

/* === Eyebrow + headline ================================== */
.eyebrow {
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-3);
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
}
.eyebrow .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ok);
}

.headline {
  font-family: var(--sans);
  font-weight: 500;
  font-size: var(--t-2xl);
  line-height: 1.25;
  letter-spacing: -0.01em;
  margin: 0;
  text-wrap: pretty;
  color: var(--text);
}
.headline em { font-style: italic; font-weight: 500; color: var(--text); }

.lede {
  color: var(--text-2);
  font-size: var(--t-md);
  line-height: 1.55;
  margin: 0;
}

/* === Form ================================================ */
.field { display: flex; flex-direction: column; gap: var(--s-2); }
.field-label {
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-3);
}
.field-help {
  font-size: var(--t-sm);
  color: var(--text-3);
  margin: 0;
}
.field-help-sm {
  font-size: var(--t-xs);
  color: var(--text-3);
  margin-top: var(--s-1);
}

.input {
  width: 100%;
  background: var(--bg-dim);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r-sm);
  padding: var(--s-2) var(--s-3);
  font-size: var(--t-md);
  line-height: 20px;
  color: var(--text);
  outline: none;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.input::placeholder { color: var(--text-3); }

.input-with-button { display: flex; gap: var(--s-2); }

/* === Buttons ============================================ */
.btn {
  font-family: var(--sans);
  font-size: var(--t-md);
  font-weight: 500;
  line-height: 20px;
  padding: var(--s-2) var(--s-4);
  border-radius: var(--r-sm);
  border: 1px solid transparent;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  transition: background 0.12s ease, border-color 0.12s ease,
              color 0.12s ease;
  white-space: nowrap;
}
.btn:active { transform: translateY(1px); }

/* Disabled. The stylesheet had no rule for this at all, so a disabled button
   rendered pixel-identical to a live one — same fill, same text colour, same
   `cursor: pointer` — and the upload submit ships disabled on first paint.
   Sits above .is-pending because a pending control is busy, not unavailable,
   and the two must not look the same. */
.btn:disabled,
.btn[disabled],
.btn[aria-disabled="true"],
.input:disabled,
.input[disabled],
select.input:disabled,
button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
}
/* Kill the hover response — a control that can't be pressed shouldn't answer.
   Each variant restores its OWN resting value: a blanket `background: inherit`
   resolved to the card background, so a disabled primary button visibly
   changed on hover, which is the opposite of the intent. These sit at 0-3-0
   and so beat the 0-2-0 variant hovers regardless of source order. */
.btn:disabled:hover,
.btn[disabled]:hover { filter: none; }
.btn-primary:disabled:hover,
.btn-primary[disabled]:hover   { background: var(--accent);   border-color: var(--accent); }
.btn-secondary:disabled:hover,
.btn-secondary[disabled]:hover { background: var(--surface-2); color: var(--text); }
.btn-ghost:disabled:hover,
.btn-ghost[disabled]:hover     { background: transparent; border-color: transparent; color: var(--text-2); }
.btn-danger:disabled:hover,
.btn-danger[disabled]:hover    { filter: none; }

/* In-flight submit: dimmed, non-interactive, with a spinner in place of
   the usual leading icon. Applied by app.js on form submit. */
.btn.is-pending {
  opacity: 0.72;
  cursor: progress;
  pointer-events: none;
}
.btn.is-pending::before {
  content: "";
  width: 12px;
  height: 12px;
  margin-right: var(--s-0);
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: btn-spin 0.6s linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .btn.is-pending::before { animation-duration: 2.4s; }
}

.btn-primary {
  background: var(--accent);
  color: #fff;
  border: 1px solid var(--accent);
}
.btn-primary:hover {
  background: var(--accent-2);
  border-color: var(--accent-2);
}

.btn-secondary {
  background: var(--surface-2);
  border: 1px solid var(--hairline-strong);
  color: var(--text);
}
.btn-secondary:hover {
  background: var(--surface-3);
  color: var(--text);
}

.btn-ghost {
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-2);
}
.btn-ghost:hover {
  color: var(--text);
  background: var(--surface);
  border-color: var(--hairline);
}

.btn-danger {
  /* Not var(--bad): #fff on #f43f5e is 3.4:1, under AA, on the one control
     that destroys things. This is the same hue darkened until the label
     clears 4.5:1, with --bad kept as the border so the button still reads as
     the same red as every other danger affordance. */
  background: #b81238;
  color: #fff;
  border: 1px solid var(--bad);
}
.btn-danger:hover { filter: brightness(1.08); }

.btn-danger-outline {
  background: transparent;
  border: 1px solid var(--hairline-strong);
  color: var(--text-3);
}
.btn-danger-outline:hover,
.btn-danger-outline:focus-visible {
  background: rgba(244, 63, 94, 0.08);
  border-color: var(--bad);
  color: var(--bad);
}

.btn-block { width: 100%; }
.btn-justify { justify-content: space-between; }
.btn-icon-sm {
  padding: var(--s-2) var(--s-2);
  background: var(--surface-2);
  border: 1px solid var(--hairline-strong);
  color: var(--text-2);
}
.btn-icon-sm:hover { background: var(--surface-3); color: var(--text); }

/* === Alerts ============================================= */
.alert {
  border: 1px solid;
  border-radius: var(--r-sm);
  padding: var(--s-3) var(--s-4);
  font-size: var(--t-md);
  line-height: 1.5;
}
.alert-error {
  background: rgba(244, 63, 94, 0.08);
  border-color: rgba(244, 63, 94, 0.30);
  color: var(--bad);
}
.alert-success {
  background: rgba(74, 222, 128, 0.08);
  border-color: rgba(74, 222, 128, 0.30);
  color: var(--ok);
}
.alert ul { margin: 0; padding-left: var(--s-4); }

/* === Well (boxed disclosure region) ====================== */
.well {
  padding: var(--s-3) var(--s-4);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
}

/* === Footer / version line =============================== */
.footer-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  color: var(--text-3);
  letter-spacing: 0.06em;
}
.hairline-divider {
  height: 1px;
  background: var(--hairline);
  margin: var(--s-4) 0;
}
.divider-thin {
  height: 1px;
  background: var(--hairline);
  margin: var(--s-1) 0;
}

/* === Utility ============================================ */
.stack > * + * { margin-top: var(--gap, var(--s-4)); }
.stack-sm { --gap: var(--s-2); }
.stack-md { --gap: var(--s-4); }
.stack-lg { --gap: var(--s-6); }
.stack-xl { --gap: var(--s-7); }
.text-center { text-align: center; }
.section-gap { margin-bottom: var(--s-6); }

.icon {
  width: 1em;
  height: 1em;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
  flex-shrink: 0;
  vertical-align: -0.15em;
}

/* =========================================================
   App shell (sidebar + topbar + content)
   ========================================================= */
.shell-app {
  display: grid;
  grid-template-columns: 232px 1fr;
  min-height: 100dvh;
}

/* ----- Sidebar ---------------------------------------- */
.sidebar {
  position: sticky;
  top: 0;
  height: 100dvh;
  background: var(--bg-dim);
  border-right: 1px solid var(--hairline);
  display: flex;
  flex-direction: column;
  padding: var(--s-4) var(--s-3) var(--s-3);
  gap: var(--s-1);
  overflow-y: auto;
}
.brand {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-1) var(--s-3) var(--s-4);
}
.brand-mark-sm {
  width: 32px;
  height: 32px;
  margin: 0;
  border-radius: 7px;
}
.brand-mark-sm svg { width: 16px; height: 16px; }
.brand-text { line-height: 1.15; }
.brand-name {
  font-family: var(--sans);
  font-size: var(--t-md);
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--text);
}
.brand-sub {
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-top: var(--s-0);
}

.nav { display: flex; flex-direction: column; }
.nav-group { display: flex; flex-direction: column; gap: 1px; margin-top: var(--s-0); }
.nav-label {
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-3);
  padding: var(--s-4) var(--s-3) var(--s-2);
}
.nav-item {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  font-size: var(--t-md);
  color: var(--text-2);
  cursor: pointer;
  border: 1px solid transparent;
  background: transparent;
  text-align: left;
  transition: background 0.12s ease, color 0.12s ease;
}
.nav-icon { opacity: 0.8; }
.nav-item:hover { color: var(--text); background: var(--surface); }
.nav-item-active {
  color: var(--text);
  background: var(--surface-2);
  box-shadow: inset 2px 0 0 var(--accent);
}
.nav-item-active .nav-icon { opacity: 1; color: var(--accent); }
/* A destination this operator's role can't open. Quieter than an enabled row
   but still legible — the point is to show the capability exists, not to hide
   it. The lock trails the label so the row still scans as its own name first. */
.nav-item-locked {
  color: var(--text-3);
  cursor: not-allowed;
}
.nav-item-locked:hover { background: transparent; color: var(--text-3); }
.nav-item-locked .nav-icon { opacity: 0.45; }
.nav-item-locked .nav-icon:last-child { margin-left: auto; opacity: 0.7; }

.sidebar-footer {
  margin-top: auto;
  border-top: 1px solid var(--hairline);
  padding-top: var(--s-3);
  display: flex;
  align-items: center;
  gap: var(--s-3);
}
.avatar {
  width: 28px;
  height: 28px;
  border-radius: 6px;
  background: var(--accent);
  display: grid;
  place-items: center;
  font-family: var(--sans);
  font-size: var(--t-sm);
  font-weight: 600;
  color: #fff;
}
.user-meta { line-height: 1.15; }
.user-name { font-size: var(--t-sm); color: var(--text); }
.user-role {
  font-size: var(--t-2xs);
  color: var(--text-3);
  font-family: var(--mono);
  letter-spacing: 0.08em;
}
.icon-btn {
  margin-left: auto;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border-radius: 6px;
  background: transparent;
  border: 1px solid transparent;
  cursor: pointer;
  color: var(--text-3);
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.icon-btn:hover {
  color: var(--text);
  background: var(--surface);
  border-color: var(--hairline);
}
.logout-form { margin-left: auto; }

/* ----- Main column ------------------------------------- */
.main {
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-3) var(--s-7);
  border-bottom: 1px solid var(--hairline);
  background: var(--bg);
}
.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--t-sm);
  color: var(--text-3);
  font-family: var(--mono);
  /* A deep doc-root path has no break opportunities and would otherwise widen
     the topbar past the viewport, taking the document with it. */
  overflow-wrap: anywhere;
  min-width: 0;
}
.crumbs .sep { color: var(--text-4); }
.crumbs .here { color: var(--text); }

.topbar-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--s-2);
}
.status-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-1) var(--s-3);
  border-radius: 999px;
  font-family: var(--mono);
  font-size: var(--t-xs);
  border: 1px solid;
}
.status-pill .dot-ok,
.status-pill .dot-warn {
  width: 6px;
  height: 6px;
  border-radius: 50%;
}
.status-pill .dot-ok   { background: var(--ok); }
.status-pill .dot-warn { background: var(--warn); }
.status-ok {
  background: rgba(74, 222, 128, 0.08);
  border-color: rgba(74, 222, 128, 0.30);
  color: var(--ok);
}
.status-warn {
  background: rgba(251, 191, 36, 0.08);
  border-color: rgba(251, 191, 36, 0.30);
  color: var(--warn);
}

/* ----- Content ----------------------------------------- */
.content {
  padding: var(--s-6) var(--s-7) var(--s-9);
  max-width: 1480px;
  width: 100%;
  margin: 0 auto;
}

/* ----- Page hero (compact admin header) ----------------
   No kicker above the title: the breadcrumb in the topbar already says where
   you are, and a third restatement of it was pure noise. Title, one line of
   lede, stats on the right. */
.page-hero {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-7);
  padding-bottom: var(--s-4);
  margin-bottom: var(--s-6);
  border-bottom: 1px solid var(--hairline);
}
.hero-title {
  font-family: var(--sans);
  font-weight: 600;
  font-size: var(--t-2xl);
  line-height: 1.2;
  letter-spacing: -0.01em;
  margin: 0 0 var(--s-2);
  color: var(--text);
}
.hero-title em { font-style: italic; font-weight: 600; color: var(--text); }
.page-hero .lede { max-width: 68ch; }
.hero-stats {
  display: flex;
  gap: var(--s-7);
  text-align: right;
  flex-shrink: 0;
}
.hero-stat { flex: none; white-space: nowrap; }
.hero-stat .k {
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.16em;
  color: var(--text-3);
  text-transform: uppercase;
  margin-bottom: var(--s-1);
}
.hero-stat .v {
  font-family: var(--mono);
  font-size: var(--t-xl);
  line-height: 1;
  color: var(--text);
}
.hero-stat .d {
  font-family: var(--mono);
  font-size: var(--t-2xs);
  color: var(--text-3);
  margin-top: var(--s-1);
}

/* ----- Card head / body --------------------------------- */
.card-head {
  padding: var(--s-4) var(--s-5);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-4);
  border-bottom: 1px solid var(--hairline);
}
.card-title {
  font-family: var(--sans);
  font-size: var(--t-lg);
  font-weight: 600;
  letter-spacing: -0.005em;
  margin: 0;
  color: var(--text);
}
.card-sub {
  font-size: var(--t-sm);
  color: var(--text-3);
  margin-top: var(--s-1);
  line-height: 1.5;
}
.card-body { padding: var(--s-5); }

/* === Tables ==============================================
   The panel is mostly tables, so they get a real component. Views used
   to hand-style every cell, which is why no two tables agreed on
   padding, size or muted-text colour. Reach for `.table` + the cell
   modifiers below instead of a `style=` attribute.

     <table class="table">
       <thead><tr><th>name</th><th class="num">size</th></tr></thead>
       <tbody>
         <tr>
           <td class="key">wp_main <span class="tag tag-accent">wordpress</span></td>
           <td class="num mut">4.2 MB</td>
         </tr>
       </tbody>
     </table>

   Cell modifiers: .key (primary identifier) · .num (right + tabular
   figures) · .mut (secondary) · .dim (tertiary) · .actions (row actions)
   ========================================================= */
.table { width: 100%; border-collapse: collapse; }

.table thead th {
  background: var(--surface-2);
  font-family: var(--mono);
  font-size: var(--t-xs);
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-3);
  text-align: left;
  padding: var(--s-3) var(--s-4);
  white-space: nowrap;
}

.table tbody td {
  padding: var(--s-3) var(--s-4);
  font-family: var(--mono);
  font-size: var(--t-sm);
  color: var(--text-2);
  border-bottom: 1px solid var(--hairline);
  vertical-align: middle;
}
/* No trailing rule — the card border already closes the block. */
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr { transition: background 0.1s ease; }
.table tbody tr:hover { background: var(--surface-2); }

/* Cell modifiers. Listed twice on purpose: the bare class is a general
   text utility usable anywhere, and the `.table tbody td` form re-states it
   at higher specificity so it still wins inside a table cell. */
.key,             .table tbody td.key { font-size: var(--t-md); color: var(--text); }
/* A human name, not an identifier. "Advanced Custom Fields" is prose and
   belongs in the text face; slugs, versions, paths and byte counts stay mono
   because those are things you compare, copy or type. Setting a whole table
   in mono because the subject is technical is a costume. */
.table tbody td.name {
  font-family: var(--sans);
  font-size: var(--t-md);
  font-weight: 500;
  color: var(--text);
}
.table tbody td.name .desc {
  font-weight: 400;
  font-size: var(--t-sm);
  color: var(--text-3);
  margin-top: var(--s-1);
  max-width: 420px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mut,             .table tbody td.mut { color: var(--text-3); }
.dim,             .table tbody td.dim { color: var(--text-3); }
.ok,              .table tbody td.ok  { color: var(--ok); }
.bad,             .table tbody td.bad { color: var(--bad); }
.num,             .table tbody td.num {
  text-align: right;
  /* Digits share one advance width, so columns of numbers line up. */
  font-variant-numeric: tabular-nums;
}
.table .actions { text-align: right; font-size: var(--t-xs); white-space: nowrap; }
@media (max-width: 900px) {
  /* nowrap keeps rows one line high on desktop; on a phone it is the thing
     that pushes the row past the card edge. */
  .table .actions { white-space: normal; }
  /* A head whose title and button will not fit side by side stacks instead of
     letting the button slide out of the card. */
  .card-head { flex-wrap: wrap; }
}

/* Clip a long path/name to one line rather than wrapping the row. */
.truncate {
  max-width: 380px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Inline label attached to a cell value (wordpress / system / user). */
.tag {
  display: inline-block;
  margin-left: var(--s-2);
  padding: 1px var(--s-2);
  border-radius: 999px;
  font-family: var(--mono);
  font-size: var(--t-2xs);
  line-height: 1.6;
  background: var(--surface-3);
  color: var(--text-3);
  vertical-align: middle;
}
.tag-accent { background: var(--accent-soft);            color: var(--accent-2); }
.tag-info   { background: rgba(56, 189, 248, 0.10);      color: var(--info); }
.tag-warn   { background: rgba(251, 191, 36, 0.10);      color: var(--warn); }

/* Bare text button for row actions — looks like a link, submits a form. */
.btn-inline {
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  font-family: var(--mono);
  font-size: var(--t-xs);
  color: var(--text-2);
  transition: color 0.12s ease;
}
.btn-inline:hover { color: var(--text); }
.btn-inline-danger { color: var(--bad); }
.btn-inline-danger:hover { filter: brightness(1.15); }
/* Destructive but not deletion — restore replaces files AND database in place.
   It used to render as the lightest control on its row despite being the
   heaviest action on it. */
.btn-inline-warn { color: var(--warn); }
.btn-inline-warn:hover { filter: brightness(1.15); }
.actions-sep { color: var(--text-3); padding: 0 var(--s-1); }
.link-accent { color: var(--accent-2); }
.link-accent:hover { color: var(--accent-2); filter: brightness(1.15); }
/* Keeps a one-button POST form inline with sibling action links. */
.inline-form { display: inline; }

/* Table that fills its card edge-to-edge. */
/* Every table in the panel sits in one of these. `.card` is `overflow: hidden`,
   so without a scroller here a table wider than its card was simply cut off
   with no way to reach the rest — at 390px the accounts table rendered 856px
   wide and lost its last three columns AND every row action. Scrolling the
   table rather than the page keeps the body from overflowing sideways.
   `-webkit-overflow-scrolling` keeps the momentum scroll on iOS Safari. */
.card-body-flush {
  padding: 0;
  overflow-wrap: normal;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
/* Body holding a .meta list — the rows supply their own vertical rhythm. */
.card-body-gutter { padding: 0 var(--s-5); }

/* Small bordered surface: a toolbar segment, a path crumb, a count badge. */
.subtle-box {
  padding: var(--s-2) var(--s-3);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
}

/* Boxed inline code token (a granted db name, a grant pattern). */
.chip {
  display: inline-block;
  font-family: var(--mono);
  font-size: var(--t-xs);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  border-radius: 4px;
  padding: 1px var(--s-2);
  margin-right: var(--s-1);
}
.chip-accent {
  background: rgba(192, 38, 211, 0.10);
  border-color: rgba(192, 38, 211, 0.30);
  color: var(--accent-2);
}

/* === Modal (native <dialog>) =============================
   Replaces window.prompt/alert/confirm. showModal() supplies the focus
   trap, Esc handling and inert background, so this is purely visual.
   ========================================================= */
.modal {
  padding: 0;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r-md);
  background: var(--surface);
  color: var(--text);
  width: min(440px, calc(100vw - 32px));
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.55);
}
.modal::backdrop {
  background: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(2px);
}
.modal-form { padding: var(--s-6); display: grid; gap: var(--s-4); }
.modal-title {
  margin: 0;
  font-size: var(--t-xl);
  font-weight: 600;
  letter-spacing: -0.01em;
}
.modal-body {
  margin: 0;
  font-size: var(--t-md);
  line-height: 1.55;
  color: var(--text-2);
  /* Confirmation copy sometimes carries a second line of consequence. */
  white-space: pre-line;
}
.modal-fields { display: grid; gap: var(--s-3); }

/* Checkbox list inside a modal (e.g. which databases a user may access). */
.check-group {
  display: grid;
  gap: var(--s-0);
  max-height: 260px;
  overflow-y: auto;
  border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
  padding: var(--s-2) var(--s-3);
}
.check-group .field-label { margin-bottom: var(--s-1); }
/* .check-label carries a top margin for its standalone use under a form;
   inside a list the rows sit flush. */
.check-row { margin-top: 0; padding: var(--s-1) var(--s-0); font-family: var(--mono); }
.check-row:hover { color: var(--text); }
.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--s-2);
  margin-top: var(--s-0);
}

/* File upload queue. Each file gets its own real progress and result. */
.upload-modal { width: min(640px, calc(100vw - 32px)); }
.upload-modal-inner { padding: var(--s-6); display: grid; gap: var(--s-4); }
.upload-modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s-4); }
.upload-summary { margin: var(--s-1) 0 0; color: var(--text-3); font-size: var(--t-sm); }
.upload-total-percent { color: var(--text-2); font-family: var(--mono); font-size: var(--t-md); }
.upload-total-track,
.upload-file-track { height: 5px; overflow: hidden; border-radius: 999px; background: var(--surface-3); }
.upload-total-track span,
.upload-file-track span { display: block; width: 100%; height: 100%; border-radius: inherit; background: var(--accent-2); transform: scaleX(0); transform-origin: left; transition: transform 0.15s ease-out; }
.upload-file-list { display: grid; gap: var(--s-2); max-height: min(50vh, 420px); overflow-y: auto; }
.upload-file { padding: var(--s-3); border: 1px solid var(--hairline); border-radius: var(--r-sm); background: var(--surface-2); }
.upload-file-top { display: flex; justify-content: space-between; gap: var(--s-3); }
.upload-file-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); font-size: var(--t-sm); }
.upload-file-state { flex: none; color: var(--text-3); font-size: var(--t-xs); }
.upload-file-meta { margin: var(--s-1) 0 var(--s-2); color: var(--text-3); font-size: var(--t-xs); }
.upload-file.is-complete .upload-file-state { color: var(--ok); }
.upload-file.is-complete .upload-file-track span { background: var(--ok); }
.upload-file.is-failed .upload-file-state { max-width: 50%; overflow: hidden; text-overflow: ellipsis; color: var(--bad); }
.upload-file.is-failed .upload-file-track span { background: var(--bad); }
.upload-file.is-cancelled { opacity: 0.65; }

/* Entry animation, skipped for prefers-reduced-motion (see below). */
@media (prefers-reduced-motion: no-preference) {
  .modal[open] { animation: modal-in 0.14s ease-out; }
  @keyframes modal-in {
    from { opacity: 0; transform: translateY(-6px) scale(0.98); }
    to   { opacity: 1; transform: none; }
  }
}

@media (max-width: 900px) {
  .modal-actions { flex-direction: column-reverse; }
  .modal-actions .btn { width: 100%; justify-content: center; }
}

/* === Tab bar ============================================= */
.tabs {
  display: flex;
  gap: var(--s-1);
  margin-bottom: var(--s-6);
  border-bottom: 1px solid var(--hairline);
}
.tab {
  padding: var(--s-3) var(--s-4);
  margin-bottom: -1px;
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-3);
  border-bottom: 2px solid transparent;
  transition: color 0.12s ease, border-color 0.12s ease;
}
.tab:hover { color: var(--text-2); }
.tab-active { color: var(--text); border-bottom-color: var(--accent); }

/* === Inline form layouts ================================= */
/* Field row that ends in a submit button. Follows .grid-2's naming: the
   suffix counts the flexible columns before the trailing auto button. */
.form-grid { display: grid; gap: var(--s-3); align-items: end; }
.form-grid-2 { grid-template-columns: 1fr 1fr auto; }
.form-grid-3 { grid-template-columns: 1fr 1fr 1fr auto; }
.form-grid-4 { grid-template-columns: 1fr 1fr 1fr auto auto; }

/* Selects render shorter than inputs at the same padding; pin them so a
   select sitting beside an input in a .form-grid lines up. */
select.input { height: var(--control-h); }
.input-narrow { max-width: 260px; }

.check-label {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-4);
  font-size: var(--t-md);
  color: var(--text-2);
  cursor: pointer;
}

.is-hidden { display: none; }

body.is-navigating { cursor: progress; }

/* ----- Loading skeleton -------------------------------- */
/* Placeholder blocks the dynamic navigator drops into <main> while a page is
   in flight. Only the blocks live here — the surrounding .topbar/.content/
   .card wrappers are the real layout classes, so the skeleton already has the
   geometry of the page replacing it. */
.sk {
  display: block;
  border-radius: var(--r-sm);
  background: linear-gradient(
    90deg,
    var(--surface-2) 25%,
    var(--surface-3) 37%,
    var(--surface-2) 63%
  );
  background-size: 400% 100%;
  animation: sk-shimmer 1.4s ease infinite;
}
.sk + .sk { margin-top: var(--s-3); }
.sk-head    { height: 26px; width: 280px; max-width: 100%; }
.sk-text    { height: 12px; width: 160px; max-width: 100%; }
.sk-text-sm { width: 110px; }
.sk-text-lg { width: 460px; }
.sk-rows .sk         { height: 14px; width: 100%; }
.sk-rows .sk:nth-child(2n) { width: 72%; }
.sk-rows .sk:nth-child(3n) { width: 86%; }

@keyframes sk-shimmer { to { background-position: -400% 0; } }
/* Matches .btn.is-pending: slowed, not removed — the page is still busy and
   that has to stay visible. */
@media (prefers-reduced-motion: reduce) {
  .sk { animation-duration: 4s; }
}
/* Detail region revealed by a .check-label checkbox. */
.check-detail { margin-top: var(--s-3); }

@media (max-width: 900px) {
  .form-grid-2, .form-grid-3, .form-grid-4 { grid-template-columns: 1fr; }
}

/* === Meta rows (key/value detail lists) ================== */
.meta { display: grid; }
.meta-row {
  display: grid;
  grid-template-columns: 140px 1fr;
  gap: var(--s-3);
  align-items: center;
  padding: var(--s-3) 0;
  border-bottom: 1px solid var(--hairline);
}
.meta-row:last-child { border-bottom: 0; }
.meta-key {
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-3);
}
.meta-val { font-family: var(--mono); font-size: var(--t-md); color: var(--text); }
.meta-val-info   { color: var(--info); }
.meta-val-accent { color: var(--accent-2); }
.meta-val-empty  { color: var(--text-3); }

@media (max-width: 900px) {
  .meta-row { grid-template-columns: 1fr; gap: var(--s-1); }
}

/* Flush-mount tables align with .card-head's 20px gutter. */
.card-body > table { width: 100%; border-collapse: collapse; }
.card-body > table th:first-child,
.card-body > table td:first-child { padding-left: var(--s-5); }
.card-body > table th:last-child,
.card-body > table td:last-child  { padding-right: var(--s-5); }
.card-body > div > table:not(.no-card-align) th:first-child,
.card-body > div > table:not(.no-card-align) td:first-child { padding-left: var(--s-5); }
.card-body > div > table:not(.no-card-align) th:last-child,
.card-body > div > table:not(.no-card-align) td:last-child  { padding-right: var(--s-5); }

.grid { display: grid; gap: var(--s-4); }
.grid-2 { grid-template-columns: 1fr 1fr; }

/* ----- Quick-action list ------------------------------ */
.qa-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}
.qa-item {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-3);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
  color: var(--text-2);
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.qa-item:hover {
  background: var(--surface-3);
  border-color: var(--hairline-strong);
  color: var(--text);
}
.qa-icon {
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: 6px;
  color: var(--text-3);
}
.qa-text { display: flex; flex-direction: column; gap: var(--s-0); flex: 1; min-width: 0; }
.qa-title { font-size: var(--t-md); font-weight: 500; color: var(--text); }
.qa-sub { font-size: var(--t-sm); color: var(--text-3); }
.qa-sub code {
  font-family: var(--mono);
  font-size: var(--t-xs);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: 4px;
  padding: 0 var(--s-1);
}
.qa-arrow { color: var(--text-4); }
.qa-item:hover .qa-arrow { color: var(--accent); }

/* ----- Activity list ---------------------------------- */
.activity-list { list-style: none; margin: 0; padding: 0; }
.activity-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-2) 0;
  border-bottom: 1px solid var(--hairline);
  font-size: var(--t-sm);
}
.activity-row:last-child { border-bottom: none; }
.activity-row code.mono {
  font-family: var(--mono);
  font-size: var(--t-xs);
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  border-radius: 4px;
  padding: 1px var(--s-2);
}
.activity-row .muted { color: var(--text-3); font-size: var(--t-sm); }
.activity-row .right { text-align: right; }

/* ----- Site cards (sites.php) --------------------------
   The preview used to be the card: a 16:10 screenshot taking ~70% of the
   area, with the six actions squeezed into a 280px rail beside it and a large
   void underneath. On a page whose entire job is operating a site, the
   decoration was leading and the work was in the margin.

   So the preview is now identification — thumbnail size, in the header
   next to the domain it identifies — and the actions are the body. Card
   height drops from ~660px to ~200px, which is the difference between one
   install per screen and three. */
.site-head {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  min-width: 0;
}
.site-thumb {
  border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
  overflow: hidden;
  background: var(--bg-dim);
  width: 116px;
  aspect-ratio: 16/10;
  flex: none;
  display: block;
  text-decoration: none;
  transition: border-color 0.12s ease;
}
.site-thumb:hover { border-color: var(--hairline-strong); }
.site-thumb img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; }

/* Actions read left to right in the order the job usually runs: look inside,
   then act on it. Backup is the one that changes something, so it is the one
   that carries the accent. */
.site-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;
}
.site-actions .spacer { margin-left: auto; }

/* Metadata as a spec row rather than a middot sentence: each fact gets its
   own labelled column so the eye can jump to the one it wants. */
.site-specs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-6);
  margin-top: var(--s-3);
}
.site-spec { display: flex; flex-direction: column; gap: var(--s-1); min-width: 0; }
.site-spec .k {
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-3);
}
.site-spec .v {
  font-family: var(--mono);
  font-size: var(--t-sm);
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}

.empty-state {
  text-align: center;
  padding: var(--s-8) var(--s-6);
}
.empty-state-title {
  font-family: var(--sans);
  font-size: var(--t-xl);
  font-weight: 600;
  color: var(--text);
  margin-bottom: var(--s-2);
}

.manual-db-grid {
  display: none;
  grid-template-columns: 1fr 1fr 1fr;
  gap: var(--s-3);
  align-items: end;
  margin-bottom: var(--s-4);
}
.manual-db-grid.is-open { display: grid; }
.manual-db-grid > .full {
  grid-column: span 3;
  display: flex;
  gap: var(--s-2);
  align-items: center;
  font-size: var(--t-sm);
  color: var(--text-2);
  cursor: pointer;
}

.form-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: var(--s-2);
}

/* ====================================================== */
/* Responsive                                              */
/* ====================================================== */
@media (max-width: 900px) {
  /* The sidebar used to stack its full height above the page — brand, six
     group labels, ten items and the account footer, about 470px of chrome
     before the first word of content. Narrow isn't a designed mobile
     experience here, but it does have to start with the page: the nav
     becomes one scrollable bar and everything below it is the work. */
  .shell-app { grid-template-columns: 1fr; }
  .sidebar {
    position: sticky;
    top: 0;
    z-index: 30;
    height: auto;
    flex-direction: row;
    align-items: center;
    gap: var(--s-2);
    border-right: none;
    border-bottom: 1px solid var(--hairline);
    padding: var(--s-2) var(--s-4);
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
  }
  .sidebar::-webkit-scrollbar { display: none; }
  .brand { padding: 0; gap: var(--s-2); flex: none; }
  .brand-text { display: none; }
  .nav { flex-direction: row; gap: var(--s-1); }
  .nav-group { flex-direction: row; gap: var(--s-1); margin-top: 0; }
  .nav-label { display: none; }
  .nav-item { white-space: nowrap; }
  .nav-item-active { box-shadow: none; border-color: var(--accent); }
  .sidebar-footer {
    border-top: none;
    margin-top: 0;
    margin-left: auto;
    padding-top: 0;
    flex: none;
  }
  .user-meta { display: none; }

  .content { padding: var(--s-4) var(--s-4) var(--s-8); }
  .topbar { padding: var(--s-3) var(--s-4); }
  .page-hero {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s-4);
  }
  .hero-stats { text-align: left; width: 100%; flex-wrap: wrap; }
  .grid-2 { grid-template-columns: 1fr; }
  .manual-db-grid { grid-template-columns: 1fr; }
  .manual-db-grid > .full { grid-column: auto; }

  /* Card head stacks so the domain isn't crushed against "Open site", and the
     action row runs left-to-right instead of pinning backup to the far edge. */
  .card-head { flex-direction: column; align-items: flex-start; gap: var(--s-3); }
  .card-head > div { width: 100%; }
  .site-actions .spacer { display: none; }
}

/* ====================================================== */
/* Motion preference                                       */
/* ====================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }

  /* Two exceptions, and they must sit AFTER the blanket rule above: with both
     sides !important, source order decides, and these lose everywhere else in
     the file. Both communicate "still working" — frozen, they say "broken"
     instead, which is worse than the motion they were meant to spare. Slowed,
     not removed. */
  .sk {
    animation-duration: 4s !important;
    animation-iteration-count: infinite !important;
  }
  .btn.is-pending::before {
    animation-duration: 2.4s !important;
    animation-iteration-count: infinite !important;
  }
}
