/* SixPanel — "Polar" design language. No framework, no external assets.
   ---------------------------------------------------------------------------
   A calm, airy, professional panel: soft cool-grey page, white floating
   surfaces, indigo accent, pill chips, generous spacing.

   Theme model (unchanged contract): LIGHT is the default (:root).
   html[data-theme="dark"] forces dark; no attribute = "system" and the
   prefers-color-scheme media block applies the same dark tokens. Every
   component reads the variables only — the whole theme is swappable from
   :root alone.

   Legacy variable names (--bg2, --card, --primary, --ok-fg, …) are kept as
   aliases because JS templates paint inline styles/SVG strokes with them. */

:root {
  /* ---- surfaces ---- */
  --bg: #f6f7fb;              /* page background */
  --surface: #ffffff;         /* cards, sidebar, modals */
  --surface-2: #fbfbfe;       /* subtle raised bits: hover, tiles */
  --well: #eff1f7;            /* sunken bits: meter tracks, code, crumbs */
  --border: #e7e9f2;          /* hairlines */
  --border-strong: #dcdfeb;   /* input outlines */
  /* ---- text ---- */
  --text: #101321;
  --muted: #5b6172;
  /* ---- accent (indigo) ---- */
  --accent: #4f46e5;
  --accent-strong: #4338ca;
  --accent-soft: #eef0ff;
  --accent-fg: #ffffff;       /* text on solid accent */
  /* ---- status ---- */
  --ok: #16a34a;    --ok-soft: #e8f7ee;   --ok-fg: #15803d;
  --warn: #d97706;  --warn-soft: #fdf3e2; --warn-fg: #b45309;
  --err: #dc2626;   --err-soft: #fdecec;  --err-fg: #b91c1c;
  --info: #0284c7;  --info-soft: #e7f4fb; --info-fg: #0369a1;
  /* ---- console (job logs keep a dark terminal look in BOTH themes) ---- */
  --console-bg: #0e101c;
  --console-text: #d7dbe8;
  --console-line: #23263a;
  /* ---- fixed-purpose ---- */
  --qr-bg: #ffffff;           /* QR codes must stay white to scan */
  --on-solid: #ffffff;        /* text/knob on saturated fills, both themes */
  /* ---- brand gradient (small marks only) ---- */
  --grad: linear-gradient(90deg, #6366f1, #a855f7);
  /* ---- chrome ---- */
  --overlay: rgba(13, 15, 23, .5);
  --shadow-1: 0 1px 2px rgba(16, 19, 33, .05), 0 8px 24px rgba(16, 19, 33, .05);
  --shadow-2: 0 2px 6px rgba(16, 19, 33, .07), 0 18px 48px rgba(16, 19, 33, .14);
  --shadow-knob: 0 1px 2px rgba(16, 19, 33, .22);
  --mesh-a: 16%;              /* mesh gradient strength (accent corner) */
  --mesh-b: 9%;               /* mesh gradient strength (warm corner) */
  --warm: #f59e0b;            /* the warm mesh tint */
  /* ---- shape ---- */
  --r-ctl: 10px;              /* inputs, buttons, nav pills */
  --r-card: 14px;             /* cards */
  --r-modal: 20px;            /* modals + wizard card */
  /* ---- type ---- */
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* ---- motion ---- */
  --tick: .16s ease-out;
  /* ---- legacy aliases (JS inline styles + old markup read these) ---- */
  --bg2: var(--well);
  --ring: color-mix(in srgb, var(--muted) 45%, transparent);
  --primary: var(--accent);
  --radius: var(--r-ctl);
  color-scheme: light;
}

/* explicit dark choice */
:root[data-theme="dark"] {
  --bg: #0d0f17;
  --surface: #151827;
  --surface-2: #191d30;
  --well: #10131f;
  --border: #272b40;
  --border-strong: #2f3450;
  --text: #e9eaf3;
  --muted: #9aa0b5;
  --accent: #818cf8;
  --accent-strong: #a5b4fc;
  --accent-soft: #232748;
  --accent-fg: #14163a;
  --ok-soft: #12281c;   --ok-fg: #4ade80;
  --warn-soft: #2b2113; --warn-fg: #fbbf24;
  --err-soft: #2c1519;  --err-fg: #f87171;
  --info-soft: #10222f; --info-fg: #38bdf8;
  --console-bg: #0a0c15;
  --console-line: #1e2233;
  --overlay: rgba(0, 0, 0, .62);
  --shadow-1: 0 0 0 1px rgba(39, 43, 64, .55), 0 8px 24px rgba(0, 0, 0, .35);
  --shadow-2: 0 0 0 1px rgba(47, 52, 80, .7), 0 20px 56px rgba(0, 0, 0, .55);
  --shadow-knob: 0 1px 2px rgba(0, 0, 0, .5);
  --mesh-a: 11%;
  --mesh-b: 6%;
  color-scheme: dark;
}

/* system preference (no data-theme attribute set) */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0d0f17;
    --surface: #151827;
    --surface-2: #191d30;
    --well: #10131f;
    --border: #272b40;
    --border-strong: #2f3450;
    --text: #e9eaf3;
    --muted: #9aa0b5;
    --accent: #818cf8;
    --accent-strong: #a5b4fc;
    --accent-soft: #232748;
    --accent-fg: #14163a;
    --ok-soft: #12281c;   --ok-fg: #4ade80;
    --warn-soft: #2b2113; --warn-fg: #fbbf24;
    --err-soft: #2c1519;  --err-fg: #f87171;
    --info-soft: #10222f; --info-fg: #38bdf8;
    --console-bg: #0a0c15;
    --console-line: #1e2233;
    --overlay: rgba(0, 0, 0, .62);
    --shadow-1: 0 0 0 1px rgba(39, 43, 64, .55), 0 8px 24px rgba(0, 0, 0, .35);
    --shadow-2: 0 0 0 1px rgba(47, 52, 80, .7), 0 20px 56px rgba(0, 0, 0, .55);
    --shadow-knob: 0 1px 2px rgba(0, 0, 0, .5);
    --mesh-a: 11%;
    --mesh-b: 6%;
    color-scheme: dark;
  }
}

/* ===========================================================================
   base
   ======================================================================== */

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--bg);
  color: var(--text);
  font: 14px/1.55 var(--font);
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
[hidden] { display: none !important; }

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

h1 { font-size: 22px; margin: 0; font-weight: 700; letter-spacing: -.01em; }
h2 { letter-spacing: -.01em; }
h3 { font-size: 15px; margin: 0 0 12px; font-weight: 650; letter-spacing: -.01em; }

code {
  font-family: var(--mono); font-size: .92em;
  background: var(--well); padding: 1.5px 6px; border-radius: 6px;
}
.mono { font-family: var(--mono); font-size: 12.5px; }

::selection { background: color-mix(in srgb, var(--accent) 22%, transparent); }

/* quiet, theme-following scrollbars (page scroll behavior unchanged) */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--muted) 32%, transparent);
  border-radius: 99px; border: 2px solid transparent; background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background-color: color-mix(in srgb, var(--muted) 50%, transparent); }

.hint { color: var(--muted); margin: 4px 0 0; font-size: 13px; }
.spacer { flex: 1; }

/* ===========================================================================
   shell layout: sidebar + main column
   ======================================================================== */

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

.sidebar {
  width: 232px; flex: 0 0 232px;
  background: var(--surface);
  border-inline-end: 1px solid var(--border);
  display: flex; flex-direction: column;
  padding: 18px 14px 14px;
  position: sticky; top: 0; height: 100vh;
}

/* brand block: mark + the name as type + a small gradient beacon */
.brand { display: flex; align-items: center; gap: 12px; padding: 4px 10px 3px; }
.brand::after {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--grad); margin-inline-start: 1px; flex: 0 0 auto;
  align-self: flex-start; margin-top: 2px;
}
/* 36px, not 26. Measured against the 232px sidebar it sits in: at 26 beside a
   14px wordmark the whole lockup was smaller than one nav row's text and read as
   an afterthought. 36/19 fills the column without crowding it. */
.brand-icon { width: 36px; height: 36px; flex: 0 0 auto; }
/* The name as TYPE. Sized in px against the 36px mark beside it, weight 700 to
   match .login-title, and -.01em tracking so "SixPanel" closes up the way the
   login card's does. "Six" takes the brand indigo; the rest inherits --text, so
   both themes are the page's job with no second file and no swap. */
.brand-name {
  font-size: 19px; font-weight: 700; letter-spacing: -.01em;
  color: var(--text); line-height: 1; white-space: nowrap;
}
.brand-name-a { color: var(--primary); }
/* 58px = the sidebar's 10px padding + the 36px mark + the 12px gap, so the
   version starts exactly under the "S". */
.brand-sub {
  color: var(--muted); font-size: 10.5px; font-weight: 650;
  letter-spacing: .06em; text-transform: uppercase;
  padding: 2px 10px 18px 58px;
  /* the version, the update chip and (for a demo or temporary session) the
     scope chip share this line inside a 240px sidebar — let them sit in a row
     that wraps, instead of overflowing the column */
  display: flex; align-items: center; flex-wrap: wrap; gap: 5px;
}
/* the scope chip lives here too: same pill, one size down, and it never
   stretches the line's height */
#scopeChip { font-size: 9.5px; padding: 2px 8px; letter-spacing: .04em; }
#scopeChip::before { width: 5px; height: 5px; }
/* the running version: a fact to look up, not a headline — and never
   uppercased (a version is not a word) */
/* no margin of its own: .brand-sub is a flex row now and owns the spacing */
.brand-ver { text-transform: none; letter-spacing: 0; opacity: .85; }
/* update chip appended next to the version; it is an <a> to Settings */
.brand-ver-chip { margin-inline-start: 6px; text-decoration: none; }
.brand-ver-chip:hover { text-decoration: none; filter: brightness(1.05); }
/* big variant (setup wizard head) */
.brand.big { justify-content: center; font-size: 18px; font-weight: 700; padding: 0 0 10px; }
.brand.big .brand-icon { width: 38px; height: 38px; }
.brand.big::after { align-self: center; margin-top: -12px; }

/* ---- nav: rounded pills, 38px tall ---- */
.nav { display: flex; flex-direction: column; gap: 2px; flex: 1; overflow-y: auto; padding: 2px; }
.nav a {
  position: relative;
  display: flex; align-items: center; gap: 10px;
  min-height: 38px; padding: 0 12px;
  color: var(--muted); border-radius: var(--r-ctl);
  font-weight: 550; text-decoration: none;
  transition: background var(--tick), color var(--tick);
}
.nav a svg { flex: 0 0 auto; opacity: .9; }
.nav a:hover { color: var(--text); background: var(--surface-2); text-decoration: none; }
:root[data-theme="dark"] .nav a:hover { background: var(--well); }
.nav a.active { color: var(--accent); background: var(--accent-soft); font-weight: 600; }
.nav a.active svg { color: var(--accent); opacity: 1; }
/* active marker: a rounded 3px accent bar hugging the pill's start edge */
.nav a.active::before {
  content: ""; position: absolute; inset-inline-start: 0; top: 9px; bottom: 9px;
  width: 3px; border-radius: 99px; background: var(--accent);
}
.nav a:focus-visible {
  outline: none;
  box-shadow: 0 0 0 1px var(--accent), 0 0 0 4px var(--accent-soft);
}
.sidebar-foot { padding-top: 12px; }

/* nav groups (Panel / Project) */
.nav-group { display: flex; flex-direction: column; gap: 2px; }
.nav-group + .nav-group { margin-block-start: 20px; }
.nav-group-title {
  color: var(--muted); font-size: 11px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
  padding: 0 12px; margin-block-end: 6px;
}
.nav-group-head {
  display: flex; align-items: center; gap: 8px;
  margin-block-end: 6px; padding-inline-end: 2px; min-width: 0;
}
.nav-group-head .nav-group-title { flex: 0 0 auto; margin: 0; }

/* contextual nav: the back-to-panel link on project pages */
#nav a.nav-back { margin-bottom: 12px; opacity: .8; }
#nav a.nav-back:hover { opacity: 1; }

/* ---- main column ---- */
.main { flex: 1; min-width: 0; padding: 0 36px 120px; max-width: 1200px; }

/* ---- topbar: sticky translucent page head, hairline below ---- */
.page-head {
  position: sticky; top: 0; z-index: 40;
  margin: 0 -36px 26px; padding: 11px 36px;
  min-height: 56px;
  display: flex; align-items: center; justify-content: space-between;
  gap: 14px; flex-wrap: wrap;
  /* 96%, not 80%. The blur is what was doing the hiding, and a backdrop filter
     is the first thing a browser drops when it has no GPU compositing to spend
     — measured in headless Chrome, which reports backdrop-filter as supported
     and then does not paint it: the page scrolled underneath and a Restart
     button showed through the page title. @supports cannot catch that, because
     nothing there is unsupported. So the background is opaque enough to stand
     on its own and the blur stays as the effect it always was, not as the
     thing legibility depends on. */
  background: color-mix(in srgb, var(--bg) 96%, transparent);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
}
/* the title column takes the room and wraps its own hint; the tools keep their row.
   Without flex:1 a long hint made the column as wide as the line, and the moment the
   job pill joined .head-tools the tools wrapped UNDER the title, left-aligned
   (Projects, fresh demo box, 30 Sep 2026). Phones still stack, below. */
.page-head-text { flex: 1 1 0; min-width: 0; }
.page-title-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.page-head .hint { margin-top: 2px; }

/* header tools (project switcher pill, language, theme) */
.head-tools { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }

/* THE PROJECT SWITCHER (index.html #projMenu, core.js paintProjMenu / projMenuOpen): a
   pill that names the project being looked at, and a box under it listing every
   project. It was a native <select>; the pill keeps that one's shape and its place in
   the row, and is still the one thing in the row that gives way (min-width 0 — the
   buttons beside it are flex:none). The name is cut with an ellipsis and whole in the
   pill's title. */
.proj-menu { position: relative; display: flex; flex: 0 1 auto; min-width: 0; max-width: 220px; }
.proj-menu-btn {
  display: inline-flex; align-items: center; gap: 6px; flex: 0 1 auto; min-width: 0; overflow: hidden;
  min-height: 34px; padding-block: 5px; padding-inline: 14px 10px;
  border-radius: 999px; border: 1px solid var(--border-strong);
  background: var(--surface); color: var(--text);
  font: inherit; font-size: 13px; cursor: pointer;
  transition: background var(--tick), border-color var(--tick);
}
.proj-menu-btn:hover { border-color: var(--ring); background: var(--surface-2); }
.proj-menu-btn:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.proj-menu-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.proj-menu-btn svg { flex: none; color: var(--muted); }
/* Under the pill, from its end edge inwards — where the room is, as for the bell and the
   language menu. The list scrolls inside the box; the search field stays above it. */
.proj-menu-pop {
  position: absolute; top: calc(100% + 6px); inset-inline-end: 0; z-index: 80;
  width: min(340px, calc(100vw - 32px)); padding: 6px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 12px; box-shadow: var(--shadow-2);
  animation: rise-in .16s ease-out; text-align: start;
}
.proj-menu-q { width: 100%; margin-bottom: 6px; }
.proj-menu-list { max-height: min(60vh, 500px); overflow-y: auto; overscroll-behavior: contain; }
.proj-menu-group + .proj-menu-group { margin-top: 4px; padding-top: 4px; border-top: 1px solid var(--border); }
/* a group's name as it is written — never upper-cased: "6amMart" is a name */
.proj-menu-title { padding: 6px 10px 4px; color: var(--muted); font-size: 11.5px; font-weight: 700; }
.proj-menu-item {
  display: flex; align-items: center; gap: 9px; min-height: 44px;
  padding: 6px 10px; border-radius: 8px;
  color: var(--text); font-size: 13px; text-decoration: none;
  transition: background var(--tick);
}
.proj-menu-item:hover, .proj-menu-item:focus-visible { background: var(--accent-soft); text-decoration: none; outline: none; }
.proj-menu-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.proj-menu-rowname, .proj-menu-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.proj-menu-item.on .proj-menu-rowname { color: var(--accent); font-weight: 600; }
.proj-menu-sub { color: var(--muted); font-size: 11.5px; }
.proj-menu-item .badge { flex: none; }
.proj-menu-check { flex: none; width: 12px; color: var(--accent); font-size: 12px; }
.proj-menu-none { margin: 8px 10px; }
@media (prefers-reduced-motion: reduce) { .proj-menu-pop { animation: none; } }
@media print { .proj-menu { display: none !important; } }

.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  /* SQUARE, AND NOT NEGOTIABLE. `width` alone is a starting size in a flex row:
     .head-tools is `display:flex`, so once the project switcher pill joins it
     on a project page the row runs out of space and the browser shrinks the
     buttons — measured at 21.84px against 34px on the panel pages, i.e. the
     same 34px-tall button squeezed into an oval with its 16px glyph pressed
     against the left and right edges. THAT is what "the icon is cut inside a
     project" was. flex:none takes the button out of the shrink calculation;
     the pill beside it has min-width:0 and is what gives way instead. */
  flex: none; width: 34px; height: 34px; border-radius: var(--r-ctl);
  border: 1px solid var(--border-strong); background: var(--surface); color: var(--muted);
  cursor: pointer; font: inherit; padding: 0;
  transition: background var(--tick), color var(--tick), border-color var(--tick);
}
.icon-btn:hover { color: var(--text); border-color: var(--ring); background: var(--surface-2); text-decoration: none; }
/* The icon is an inline <svg>: left inline it sits on the TEXT BASELINE, so at
   some browser zoom levels it lands a few rounded pixels low and the bottom of
   the glyph clips against the button edge (reported as "the icon is cut").
   display:block hands centring entirely to the flex button. */
.icon-btn svg { display: block; width: 16px; height: 16px; flex: none; pointer-events: none; }
.icon-btn:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }

/* footer credit (shell + login) */
.app-foot {
  margin-top: 56px; padding-top: 18px;
  border-top: 1px solid var(--border);
  color: var(--muted); font-size: 12.5px;
}
.app-foot a, .foot-note a { color: var(--muted); font-weight: 600; }
.app-foot a:hover, .foot-note a:hover { color: var(--text); text-decoration: underline; }
.foot-note { color: var(--muted); font-size: 12.5px; text-align: center; margin: 20px 0 0; }

/* ===========================================================================
   cards & grid
   ======================================================================== */

.card {
  background: var(--surface);
  border: 0;
  border-radius: var(--r-card);
  padding: 20px;
  margin-bottom: 18px;
  box-shadow: var(--shadow-1);
}
.card > .hint { margin: -6px 0 14px; }
.card h2, .card h3 { font-size: 15px; font-weight: 650; }
.grid { display: grid; gap: 18px; margin-bottom: 18px; }
.grid > .card { margin-bottom: 0; }
.cols2 { grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)); }

details.card > summary {
  cursor: pointer; font-weight: 650; font-size: 15px; list-style: none;
  display: flex; align-items: center; gap: 8px;
}
details.card > summary::-webkit-details-marker { display: none; }
details.card > summary::before {
  content: "▸"; color: var(--muted); transition: transform var(--tick);
}
details.card[open] > summary::before { transform: rotate(90deg); }
details.card > summary .hint { font-weight: 400; margin: 0; }
details.card[open] > summary { margin-bottom: 14px; }

/* collapsible group: title left, its state chip right */
.grp > summary { flex-wrap: wrap; }
/* Title and chips may shrink and wrap: a state chip carrying a sentence ("Not
   chosen yet — the panel is behaving as if every domain is in one Cloudflare
   account") was nowrap and made Settings → Cloudflare 589px wide on a 390px phone
   (demo box, 30 Sep 2026). */
/* …and the title keeps the marker's line. With the summary wrapping, a title is placed
   by its full one-line width, so a long one ("Why every project on this server runs
   behind Cloudflare") dropped to a line of its own under a lone ▸ at 390px (2 Oct 2026).
   Capped at the line less the marker and its gaps, it always fits beside the marker and
   wraps inside itself; a short title keeps its own width, so the spacer still puts the
   state chip at the end. (A growing title was tried and lost: it split the free space
   with the spacer and wrapped "Move everything to another server" for nothing.) An
   empty state box would add one more gap to the line, so it takes no place at all. */
.grp > summary .grp-title { flex: 0 1 auto; min-width: 0; max-width: calc(100% - 2.5rem); }
.grp-state:empty { display: none; }
.grp > summary .chip { white-space: normal; max-width: 100%; }
.grp-state { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.grp-why { margin: 0 0 14px; }
.grp .sub-h { margin-top: 4px; font-weight: 600; }
.grp-sec { padding: 16px 0; border-top: 1px dashed var(--border); }
.grp-sec:first-of-type { padding-top: 0; border-top: none; }

/* a plain band that names the next run of cards */
.sec-band {
  margin: 26px 0 12px; font-size: 12px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
}

.error-card {
  border: 1px solid color-mix(in srgb, var(--err) 45%, transparent);
  background: color-mix(in srgb, var(--err-soft) 45%, var(--surface));
}

/* ===========================================================================
   buttons
   ======================================================================== */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  font: 600 13px/1 var(--font);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-ctl);
  min-height: 38px; padding: 0 16px;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--tick), border-color var(--tick), color var(--tick),
              box-shadow var(--tick), transform var(--tick);
}
.btn:hover { background: var(--surface-2); border-color: var(--ring); }
.btn:active { transform: translateY(0); }
.btn:disabled { opacity: .55; cursor: not-allowed; transform: none; box-shadow: none; }
/* The clicked button, while its own request runs: a dead grey button reads as
   broken — this one says "working". The spinner is a flex item, so the .btn
   gap spaces it from the label in both directions (RTL included). */
.btn.btn-busy { opacity: .85; cursor: progress; }
.btn.btn-busy::before {
  content: ""; flex: 0 0 auto; width: 12px; height: 12px; border-radius: 50%;
  border: 2px solid currentColor; border-top-color: transparent;
  animation: spin .7s linear infinite;
}
.btn:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }

.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
.btn.primary:hover {
  background: var(--accent-strong); border-color: var(--accent-strong);
  transform: translateY(-1px);
  box-shadow: 0 6px 16px color-mix(in srgb, var(--accent) 32%, transparent);
}
.btn.primary:disabled { transform: none; box-shadow: none; }
.btn.primary:focus-visible { border-color: var(--accent-strong); box-shadow: 0 0 0 3px var(--accent-soft); }

.btn.danger { background: var(--err); border-color: var(--err); color: var(--on-solid); }
.btn.danger:hover {
  background: color-mix(in srgb, var(--err) 88%, var(--text));
  border-color: color-mix(in srgb, var(--err) 88%, var(--text));
  transform: translateY(-1px);
  box-shadow: 0 6px 16px color-mix(in srgb, var(--err) 30%, transparent);
}
.btn.danger:focus-visible { box-shadow: 0 0 0 3px var(--err-soft); }

.btn.ghost { background: transparent; border-color: transparent; color: var(--muted); }
.btn.ghost:hover { background: var(--accent-soft); border-color: transparent; color: var(--accent); }
:root[data-theme="dark"] .btn.ghost:hover { background: var(--accent-soft); }

.btn.sm { min-height: 30px; padding: 0 11px; font-size: 12px; }
.btn.wide { width: 100%; }
/* A LABEL LONGER THAN THE SCREEN WRAPS; IT DOES NOT WIDEN THE PAGE. A button is one line,
   and on a phone "Start fresh install (download code + prepare the database)" is 369 px
   of it: Deploys was 401 px wide on a 390 px screen in English, the Database page 466 px
   in German (the UI audit, 7 Oct 2026). A page wider than its screen scrolls sideways and
   carries the fixed controls off with it. Not the small buttons of a toolbar and not a
   tab: those sit in strips that scroll or wrap as a row. */
@media (max-width: 860px) {
  .btn:not(.sm):not(.tab) { white-space: normal; max-width: 100%; line-height: 1.25; padding-block: 7px; text-align: center; }
  /* …and what is pressed with a finger is 24 px either way: a checkbox row was 20 px
     tall, the assistant's Ask / Auto switch 22, the file table's own checkboxes 13 */
  .check { min-height: 24px; }
  .seg button, .aic-mode button { min-height: 24px; }
  /* …and a sentence that is a button ("2 websites · 1 6amMart project") was 20 */
  .link-btn { min-height: 24px; }
  .fm-check input[type="checkbox"] { width: 24px; height: 24px; }
}
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }

/* segmented filter tab (dashboard monitoring ranges) */
.btn.tab.on {
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--accent);
  background: var(--accent-soft);
}

/* ===========================================================================
   forms
   ======================================================================== */

.input, select, textarea {
  font: 13.5px/1.4 var(--font);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-ctl);
  padding: 8px 12px;
  min-height: 38px;
  width: 100%;
  transition: border-color var(--tick), box-shadow var(--tick);
}
textarea { font-family: var(--mono); min-height: 96px; resize: vertical; line-height: 1.55; }
.input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
/* Every plain <select> draws the same chevron as the searchable .combo and the
   project switchers. The browser's own arrow is a heavier glyph pressed to the
   edge — measured on the fresh demo box, 28 Sep 2026: the wizard's Language
   select sat under Country/Timezone (.combo) wearing a different arrow. The
   data-URI cannot read currentColor, so each theme pins its own gray (the same
   two the switchers use). */
.input::placeholder, textarea::placeholder { color: color-mix(in srgb, var(--muted) 72%, transparent); }

.field { margin-bottom: 14px; }
.field label { display: block; color: var(--muted); font-size: 12.5px; margin-bottom: 6px; font-weight: 600; }
.note, .field .note { color: var(--muted); font-size: 12px; margin-top: 5px; }
.form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 0 14px; }

.check { display: flex; align-items: center; gap: 8px; color: var(--text); font-size: 13px; margin: 5px 0; }
.check input { width: auto; min-height: 0; }
/* A step that is not optional, shown where a checkbox would be. A `checked
   disabled` input is the obvious way to render "on, and you cannot change it",
   and it is wrong: the browser greys it, and grey-with-a-tick is
   indistinguishable from grey-without at a glance — the mandatory backup in the
   maintenance window came out looking like the one task the owner had switched
   off. This is unmistakably on, and unmistakably not a control. */
.ok-tick {
  flex: 0 0 auto; width: 15px; text-align: center; color: var(--ok-fg);
  font-weight: 700; line-height: 1.5;
}
input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); }

/* small CSS switch: <label class="toggle"><input type="checkbox"><i></i></label> */
.toggle { position: relative; display: inline-flex; width: 38px; height: 22px; flex: 0 0 auto; vertical-align: middle; }
.toggle input {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; opacity: 0; cursor: pointer; min-height: 0;
}
.toggle i {
  display: block; width: 100%; height: 100%; pointer-events: none;
  background: var(--well); border: 1px solid var(--border-strong);
  border-radius: 99px;
  transition: background var(--tick), border-color var(--tick);
}
/* THE KNOB OF A SWITCH THAT IS OFF is the muted text colour, never the surface: the
   surface is what a card is made of, so in the dark theme the knob was #151827 on a
   #10131f track on a #151827 card — 1.05 : 1, measured in the browser, 10 Oct 2026. An
   off switch read as an empty capsule and could not be told from a disabled one. Muted
   is 7.1 : 1 on the dark track and 5.5 : 1 on the light one (where the white knob had
   been 1.13 : 1 and was seen by its shadow alone); on, the knob stays white on the
   accent. Held by the audit's fact on Server → Monitoring, in every mode. */
.toggle i::after {
  content: ""; position: absolute; top: 3px; inset-inline-start: 3px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--muted);
  box-shadow: var(--shadow-knob);
  transition: transform var(--tick), background var(--tick);
}
.toggle input:checked + i { background: var(--accent); border-color: var(--accent); }
.toggle input:checked + i::after { transform: translateX(16px); background: var(--on-solid); }
[dir="rtl"] .toggle input:checked + i::after { transform: translateX(-16px); }
.toggle input:focus-visible + i { box-shadow: 0 0 0 3px var(--accent-soft); }
.toggle input:disabled + i { opacity: .55; }
.toggle input:disabled { cursor: not-allowed; }

/* ===========================================================================
   tables
   ======================================================================== */

.tbl-wrap { overflow-x: auto; }
.tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
.tbl th {
  text-align: start; color: var(--muted);
  font-size: 11.5px; font-weight: 650; text-transform: uppercase; letter-spacing: .06em;
  padding: 8px 12px; border-bottom: 1px solid var(--border);
}
.tbl td { padding: 10px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; }
.tbl tr:last-child td { border-bottom: none; }
.tbl tr:hover td { background: color-mix(in srgb, var(--accent-soft) 40%, transparent); }
.tbl .mono { font-family: var(--mono); font-size: 12px; }
.tbl .actions { text-align: end; white-space: nowrap; }

/* STACKING THESE TABLES ON A PHONE MEASURED WORSE, so it is not here.
   The reasoning was sound: at 390px a project Overview's deploy history gave its
   four cells 83 / 81 / 97 / 92 px, so a commit subject wrapped inside 97px and
   each row stood 182px. Turning each row into a block gives every cell the full
   width.
   Measured on the box after doing exactly that: Recent deploys 838 -> 731px, but
   Services 922 -> 978px and its rows 104 -> 129px, because a service row's four
   short cells (name, description, state, button) each took a line of their own
   where they had shared one. Net on the page: 2839 -> 2787px, 52px for a layout
   that reads less like a table. Not worth it; reverted.
   The honest reading is that these two tables want opposite things, so a
   per-table decision would be the fix rather than a blanket breakpoint — left for
   a round that can measure each one. Recorded so the next reader does not spend
   the same hour: docs/measurements/2026-09-06.
   Services has since been decided the other way (.ssvc-tbl, below): once the last
   cell was pinned, its rows stood 367-428px at 390px with What it does and State
   hidden under the buttons (2 Oct 2026). */
.tbl .actions .btn { margin-inline-start: 6px; }

/* A ROW'S BUTTONS STAY IN REACH WHEN ITS TABLE SCROLLS SIDEWAYS. On a phone a
   four-column table is wider than its card, and .tbl-wrap scrolls it — which put
   the Remove / Restore / Sign out / Log / Run now button of every row past the
   card's edge, with nothing saying it was there (measured 30 Sep 2026 at 390px:
   15 pages, controls ending at 392-616px; `WALK_OVERFLOW=1` in browser-walk.js is
   the check). The last cell, when it holds a control, is pinned to the visible
   edge and the other cells scroll beneath it. Sticky does nothing while the table
   fits, so wide screens are unchanged; the cell needs a solid background to cover
   what scrolls under it, hover tint included. A .btn-row of buttons counts too
   (AI → Connect's Edit and Revoke ended at 479 and 498px on a 390px phone, 2 Oct). */
.tbl td:last-child:has(> .btn, > .toggle, > .btn-row) {
  position: sticky; inset-inline-end: 0; z-index: 1;
  background: var(--surface);
}
.tbl tr:hover td:last-child:has(> .btn, > .toggle, > .btn-row) {
  background: linear-gradient(color-mix(in srgb, var(--accent-soft) 40%, transparent), color-mix(in srgb, var(--accent-soft) 40%, transparent)), var(--surface);
}
.tbl-wrap { position: relative; }

/* A DATA TABLE THAT IS A LIST OF CARDS ON A PHONE (`.tbl-cards`). Measured on the GCP
   beta box at 390px (3 Oct 2026, the walk's covered-cell probe): on ten tables the
   pinned actions cell sat on top of the columns that carry the information — Problem
   reports' Status and Last reply, the databases' Belongs to / Size / Tables / Charset,
   locked-out addresses' Why / Failed tries / Clears in, PHP and Node.js versions'
   support, release dates and download sizes, MCP tokens' Last used and Calls, a
   snapshot's Contents, a signed-in device and its IP — with nothing saying the table
   scrolled. Under 600px each row becomes a card: its first cell as the title on a line
   of its own, every other value after its column's name (`data-label`, copied from the
   headings by core.js labelCardTables), the buttons last and no longer pinned. Above
   600px nothing changes. `.tbl.tbl-cards` outranks the pinned-cell rule above, hover
   included. */
@media (max-width: 600px) {
  .tbl.tbl-cards thead { display: none; }
  .tbl.tbl-cards, .tbl.tbl-cards tbody { display: block; width: 100%; }
  .tbl.tbl-cards tr {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 14px;
    padding: 10px 0; border-bottom: 1px solid var(--border);
  }
  .tbl.tbl-cards tr:last-child { border-bottom: none; }
  .tbl.tbl-cards td { display: block; padding: 0; border: 0; min-width: 0; max-width: 100%; overflow-wrap: anywhere; white-space: normal; }
  .tbl.tbl-cards td:first-child { flex: 1 1 100%; }
  .tbl.tbl-cards td:empty { display: none; }
  .tbl.tbl-cards td[data-label]:not([data-label=""]):not(:first-child)::before {
    content: attr(data-label) ": "; color: var(--muted); font-weight: 400;
    font-family: var(--font); font-size: 13px; /* a mono value keeps its own face; its name does not */
  }
  /* a row marked by a tint on each cell (this device) reads as stripes once the cells
     wrap: the card keeps one tint and one edge instead */
  .tbl.tbl-cards td, .tbl.tbl-cards tr.cur td, .tbl.tbl-cards tr.cur td:first-child { background: none; box-shadow: none; }
  .tbl.tbl-cards tr.cur { background: color-mix(in srgb, var(--ok) 7%, transparent); box-shadow: inset 3px 0 0 var(--ok); padding-inline: 10px; }
  [dir="rtl"] .tbl.tbl-cards tr.cur { box-shadow: inset -3px 0 0 var(--ok); }
  .tbl.tbl-cards td.num { text-align: start; }
  .tbl.tbl-cards td:last-child:has(> .btn, > .toggle, > .btn-row),
  .tbl.tbl-cards td.actions, .tbl.tbl-cards td.host-actions {
    position: static; flex: 1 1 100%; background: none; text-align: start; white-space: normal;
  }
  .tbl.tbl-cards td:last-child:has(> .btn, > .toggle, > .btn-row)::before { content: none; }
  .tbl.tbl-cards td .btn { margin-inline: 0 6px; }
  .tbl.tbl-cards tr:hover td, .tbl.tbl-cards tr:hover td:last-child:has(> .btn, > .toggle, > .btn-row) { background: none; }
  /* Server ▸ PHP: as a card the labelled "Used by" link has room again, so the
     plain copy under the version (the 640px rule below) would say it twice */
  .tbl.tbl-cards .rt-used-sm { display: none; }
}

/* SERVER → SERVICES IS A LIST OF CARDS ON A PHONE. As a table at 390px its buttons
   cell (two buttons, pinned) and the name cell (its scope chip does not wrap) left
   What it does its narrowest word, ~90px: every row stood 367-428px, and the pinned
   buttons covered What it does and State entirely (beta box walk, 2 Oct 2026). Here a
   row is: the name with its state beside it, what it does under them at full width,
   then its buttons. `.tbl.ssvc-tbl` outranks the pinned-cell rule above. */
@media (max-width: 600px) {
  .tbl.ssvc-tbl thead { display: none; }
  .tbl.ssvc-tbl, .tbl.ssvc-tbl tbody { display: block; }
  .tbl.ssvc-tbl tr { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 10px; padding: 12px 0; border-bottom: 1px solid var(--border); }
  .tbl.ssvc-tbl tr:last-child { border-bottom: none; }
  .tbl.ssvc-tbl td { display: block; padding: 0; border: 0; }
  .tbl.ssvc-tbl td:nth-child(3) { grid-column: 2; grid-row: 1; }
  .tbl.ssvc-tbl td:nth-child(2), .tbl.ssvc-tbl td.actions { grid-column: 1 / -1; }
  .tbl.ssvc-tbl td.actions { position: static; background: none; text-align: start; white-space: normal; }
  .tbl.ssvc-tbl td.actions:empty { display: none; }
  .tbl.ssvc-tbl td.actions .btn { margin-inline: 0 6px; }
  .tbl.ssvc-tbl tr:hover td, .tbl.ssvc-tbl tr:hover td.actions:last-child { background: none; }
}

/* AI → MONITORING'S CHECKS AND SERVER → FIREWALL'S RULESET, THE SAME WAY. Three columns
   at 390px left each ~100px: the address check listed its 25 hostnames one broken
   word at a time (beta-static.al / lsweb.net) in a 590px row, and the firewall's
   reasons ran to 444px rows (beta box walk, 2 Oct 2026). On a phone the check (with
   its hostnames) and the rule take the full width, how often / last run share the
   line under the check, and the rule's "allow from" says what it is. */
@media (max-width: 600px) {
  .tbl.ai-watch thead, .tbl.fw-plan thead { display: none; }
  .tbl.ai-watch, .tbl.ai-watch tbody, .tbl.fw-plan, .tbl.fw-plan tbody { display: block; }
  .tbl.ai-watch tr, .tbl.fw-plan tr { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 10px; padding: 10px 0; border-bottom: 1px solid var(--border); }
  .tbl.ai-watch tr:last-child, .tbl.fw-plan tr:last-child { border-bottom: none; }
  .tbl.ai-watch td, .tbl.fw-plan td { display: block; padding: 0; border: 0; }
  .tbl.ai-watch td:first-child { grid-column: 1 / -1; }
  .tbl.ai-watch td:nth-child(3) { justify-self: end; }
  .tbl.fw-plan td { grid-column: 1 / -1; }
  .tbl.fw-plan td:first-child { font-weight: 600; }
  .tbl.fw-plan td[data-label]::before { content: attr(data-label) ': '; color: var(--muted); }
  .tbl.ai-watch tr:hover td, .tbl.fw-plan tr:hover td { background: none; }
}

/* SECURITY → ACTIVITY LOG, LIKE THE ACTIVITY PAGE. Four columns at 390px wrapped every
   date to three lines and cut each detail at the card's edge (2 Oct 2026). A row is the
   sentence at full width, then the panel's id for it with the time beside it, then the
   address it came from — or nothing, rather than a lone dash. The id was the bold line
   of the card and the sentence the one under it until round 157 (UX-21); the cells are
   placed by name, so a column that moves in the page moves here with it. An entry with
   no sentence (panel.start) has its id alone. */
.tbl.sec-audit td.sec-id { color: var(--muted); font-size: 12px; }
@media (max-width: 600px) {
  .tbl.sec-audit thead { display: none; }
  .tbl.sec-audit, .tbl.sec-audit tbody { display: block; }
  .tbl.sec-audit tr { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 10px; padding: 10px 0; border-bottom: 1px solid var(--border); }
  .tbl.sec-audit tr:last-child { border-bottom: none; }
  .tbl.sec-audit td { display: block; padding: 0; border: 0; }
  .tbl.sec-audit td.sec-what { grid-column: 1 / -1; grid-row: 1; font-weight: 600; overflow-wrap: anywhere; }
  .tbl.sec-audit td.sec-what:empty { display: none; }
  .tbl.sec-audit td.sec-id { grid-column: 1; grid-row: 2; overflow-wrap: anywhere; }
  .tbl.sec-audit td.sec-when { grid-column: 2; grid-row: 2; color: var(--muted); font-size: 12px; text-align: end; }
  .tbl.sec-audit td.sec-ip { grid-column: 1 / -1; grid-row: 3; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
  .tbl.sec-audit td.sec-noip { display: none; }
  .tbl.sec-audit tr:hover td { background: none; }
}

/* THE SCHEDULED-TASK TABLES, the same way (demo box walk at 390px, 3 Oct 2026). 6amMart's
   own schedule broke "0 * * * *" over two lines and "58 minutes from now" over four; a
   website's cron and a project's own tasks pin a toggle and three or four buttons over
   the schedule, the next run and the last result. On a phone a task is: what runs (with
   its last result, or its switch) on top, when it runs and when next under it, then its
   buttons. The two with long cells WRAP (flex) rather than sit in a grid: a grid's auto
   column took the width of "any moment now · Oct 3, 2026, 12:50 AM" and left the job's
   name and command 76px, a 458px card (measured on the live page before this was kept). */
@media (max-width: 600px) {
  .tbl.cron-app thead, .tbl.cron-own thead { display: none; }
  .tbl.cron-app, .tbl.cron-app tbody, .tbl.cron-own, .tbl.cron-own tbody, .tbl.scron-tbl, .tbl.scron-tbl tbody { display: block; }
  .tbl.cron-app tr, .tbl.cron-own tr, .tbl.scron-tbl tr { gap: 4px 10px; padding: 10px 0; border-bottom: 1px solid var(--border); }
  .tbl.cron-app tr:last-child, .tbl.cron-own tr:last-child, .tbl.scron-tbl tr:last-child { border-bottom: none; }
  .tbl.cron-app td, .tbl.cron-own td, .tbl.scron-tbl td { display: block; padding: 0; border: 0; overflow-wrap: anywhere; }
  /* 6amMart's schedule: the command, then its cron line and when it is next due — short cells */
  .tbl.cron-app tr { display: grid; grid-template-columns: minmax(0, 1fr) auto; }
  .tbl.cron-app td:nth-child(2) { grid-column: 1 / -1; grid-row: 1; }
  .tbl.cron-app td:nth-child(1) { grid-row: 2; white-space: nowrap; color: var(--muted); }
  .tbl.cron-app td:nth-child(3) { grid-row: 2; text-align: end; color: var(--muted); font-size: 12.5px; }
  .tbl.cron-own tr, .tbl.scron-tbl tr { display: flex; flex-wrap: wrap; align-items: flex-start; }
  /* a project's own tasks: name + switch, when, next run, the command, where it runs */
  .tbl.cron-own td:nth-child(1) { order: 1; flex: 1 1 0; min-width: 60%; font-weight: 600; }
  .tbl.cron-own td:nth-child(6) { order: 2; margin-inline-start: auto; }
  .tbl.cron-own td:nth-child(2) { order: 3; flex: 1 1 100%; }
  .tbl.cron-own td:nth-child(5) { order: 4; flex: 1 1 100%; color: var(--muted); font-size: 12.5px; }
  .tbl.cron-own td:nth-child(3) { order: 5; flex: 1 1 100%; }
  .tbl.cron-own td:nth-child(4) { order: 6; flex: 1 1 100%; color: var(--muted); font-size: 12.5px; }
  .tbl.cron-own td.empty { flex: 1 1 100%; font-weight: 400; }
  /* a website's cron: name + command with its last result, its schedule, its next run */
  .tbl.scron-tbl td:nth-child(1) { order: 1; flex: 1 1 0; min-width: 60%; }
  .tbl.scron-tbl td:nth-child(4) { order: 2; margin-inline-start: auto; }
  .tbl.scron-tbl td:nth-child(2) { order: 3; flex: 1 1 100%; }
  .tbl.scron-tbl td:nth-child(3) { order: 4; flex: 1 1 100%; }
  /* the buttons: their own line, unpinned (`.tbl.<cls> td.x` outranks the pinned-cell rule) */
  .tbl.cron-own td.actions, .tbl.scron-tbl td.host-actions {
    order: 9; flex: 1 1 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
    position: static; background: none; text-align: start; white-space: normal;
  }
  .tbl.cron-own td.actions:empty { display: none; }
  .tbl.cron-own td.actions .btn, .tbl.scron-tbl td.host-actions .btn { margin: 0; }
  .tbl.cron-app tr:hover td, .tbl.cron-own tr:hover td, .tbl.scron-tbl tr:hover td,
  .tbl.cron-own tr:hover td.actions:last-child, .tbl.scron-tbl tr:hover td.host-actions:last-child { background: none; }
}
/* …and the same tasks on Server → Scheduled: 6amMart's schedule is .cron-app above; a
   project's own tasks (name, when, command, on/off) and each website's (with its last run and
   next run) wrap the same way — five columns scrolled the website's status out of sight. */
@media (max-width: 600px) {
  .tbl.crons-own thead, .tbl.crons-site thead { display: none; }
  .tbl.crons-own, .tbl.crons-own tbody, .tbl.crons-site, .tbl.crons-site tbody { display: block; }
  .tbl.crons-own tr, .tbl.crons-site tr { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 4px 10px; padding: 10px 0; border-bottom: 1px solid var(--border); }
  .tbl.crons-own tr:last-child, .tbl.crons-site tr:last-child { border-bottom: none; }
  .tbl.crons-own td, .tbl.crons-site td { display: block; padding: 0; border: 0; overflow-wrap: anywhere; }
  .tbl.crons-own td:nth-child(1), .tbl.crons-site td:nth-child(1) { order: 1; flex: 1 1 0; min-width: 60%; }
  .tbl.crons-own td:nth-child(1) { font-weight: 600; }
  .tbl.crons-own td:nth-child(4), .tbl.crons-site td:nth-child(5) { order: 2; margin-inline-start: auto; }
  .tbl.crons-own td:nth-child(2), .tbl.crons-site td:nth-child(2) { order: 3; flex: 1 1 100%; }
  .tbl.crons-own td:nth-child(3) { order: 4; flex: 1 1 100%; }
  .tbl.crons-site td:nth-child(3) { order: 4; }
  .tbl.crons-site td:nth-child(4) { order: 5; flex: 1 1 auto; align-self: center; color: var(--muted); font-size: 12.5px; }
  .tbl.crons-own tr:hover td, .tbl.crons-site tr:hover td { background: none; }
}

/* slow-query digest cells: clickable, one line */
.tbl td.digest { max-width: 420px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
.tbl td.digest:hover { color: var(--accent); }

/* security: active sessions table */
.sessions-tbl tr.cur td { background: color-mix(in srgb, var(--ok) 7%, transparent); }
.sessions-tbl tr.cur td:first-child { box-shadow: inset 3px 0 0 var(--ok); }
[dir="rtl"] .sessions-tbl tr.cur td:first-child { box-shadow: inset -3px 0 0 var(--ok); }

/* ===========================================================================
   chips & badges
   ======================================================================== */

.chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 600; letter-spacing: .01em;
  border-radius: 999px; padding: 3px 11px;
  border: 0; white-space: nowrap; line-height: 1.4;
}
.chip::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: 0 0 auto; }
/* ON A PHONE A CHIP MAY WRAP. It is one line by design, and some chips say a sentence:
   "off — logins allowed from anywhere" is 404 px of chip in Spanish, on a 390 px screen
   (Security → Access; the UI audit in every language, 7 Oct 2026) — the fifth chip to
   carry its page past the edge, each of the four before it given a rule of its own. A
   chip no wider than its line is exactly as it was.
   NOT in a table cell and not in a button: there the row would rather squeeze a chip than
   scroll, and a two-word chip went onto two lines because it now could ("not" over
   "created", six times on a project's Overview; "en directo" inside the Logs button).
   Those two keep the one line they had. (`width: max-content` did the same for every chip
   at once — and made a chip as unshrinkable as before to whatever it sits IN, so a
   website's card on Projects, whose chip is inside a span, was 395 px wide again on the
   test server.)
   Written BELOW the rule it changes: the same lines sat in the phone block above for a
   day and did nothing, because a later rule of the same weight wins. */
@media (max-width: 860px) {
  .chip { white-space: normal; max-width: 100%; }
  td .chip, th .chip, .btn .chip { white-space: nowrap; }
}
.chip.ok   { color: var(--ok-fg);   background: var(--ok-soft); }
.chip.warn { color: var(--warn-fg); background: var(--warn-soft); }
.chip.err  { color: var(--err-fg);  background: var(--err-soft); }
.chip.off  { color: var(--muted);   background: color-mix(in srgb, var(--muted) 12%, transparent); }
.chip.acc  { color: var(--accent);  background: var(--accent-soft); }

/* small info badges (update preview, deploy triggers) */
.badge {
  display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: .02em;
  border-radius: 7px; padding: 2.5px 8px; white-space: nowrap;
  background: color-mix(in srgb, var(--muted) 11%, transparent); color: var(--muted);
}
.badge.warn { color: var(--warn-fg); background: var(--warn-soft); }
.badge.acc  { color: var(--accent);  background: var(--accent-soft); }
.badge.info { color: var(--info-fg); background: var(--info-soft); }
.badge-row { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }
.badge svg { vertical-align: -2px; margin-inline-end: 2px; }
/* ON A PHONE THE BADGE THAT SAYS WHAT A DELIVERY DID MAY WRAP. A badge is a word or two
   and one line by design; this one says a sentence. "verificado — los despliegues estaban
   desactivados" is some 310 px of badge beside its label in a card with 326, and a shop's
   Deploys page stood 402 px wide on a 390 px screen in Spanish, 393 in Portuguese (the UI
   audit pointed at a real server, 9 Oct 2026 — a jar has no delivery to draw;
   tests/Scripts/deploys-head-phone-measure.js measures it). Only this badge: every other
   keeps its one line. Written BELOW the rule it changes. */
@media (max-width: 860px) {
  .badge.dl-result { white-space: normal; }
}

/* protected-path badge (files) */
.prot-badge { cursor: help; margin-inline-start: 6px; }

/* ===========================================================================
   metrics, stats & sparklines (dashboard)
   ======================================================================== */

.metric { margin-bottom: 14px; }
.metric-top { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px; }
.metric-top span:last-child { color: var(--muted); }
/* The meter's second line: how many cores the CPU percentage is spread across,
   or how much of a disk or memory is still free. Smaller than the pair above it
   because it answers a follow-up question, not the first one. */
.metric-sub { margin-top: 4px; font-size: 11.5px; color: var(--muted); }
.meter { height: 8px; background: var(--well); border-radius: 99px; overflow: hidden; }
.meter i { display: block; height: 100%; background: var(--accent); border-radius: 99px; transition: width .4s ease-out; }
.meter i.ok { background: var(--ok); }
.meter i.warn { background: var(--warn); }
.meter i.err { background: var(--err); }

.kv { display: flex; justify-content: space-between; gap: 12px; padding: 7px 0; font-size: 13px; border-bottom: 1px solid var(--border); }
.kv:last-child { border-bottom: none; }
.kv .k { color: var(--muted); }
/* A CHIP BESIDE A LABEL THAT TAKES TWO LINES KEEPS ITS OWN SHAPE. A flex row stretches its
   items, so a one-word chip became a 40 px lozenge ("unknown" beside "In the 6amMart admin
   panel" on a phone in Spanish): it is centred instead. And the LABEL gives way first: now
   that a chip may wrap on a phone, an even share of the squeeze put "pas encore" on two
   lines beside a long French label. Weighted like this the label takes all of it, and the
   chip narrows only once the label cannot — which is the chip that says a sentence ("off
   — logins allowed from anywhere"), the one that has to wrap. (The weight is on the label
   because a chip's own small factor stops it shrinking at all once the label is done:
   factors that sum to less than one share out only that fraction of the squeeze.) */
.kv > .chip { align-self: center; }
.kv:has(> .chip) > .k { flex-shrink: 1000; }
/* a value with chips beside it (Services ▸ live updates: the websocket domain, "https",
   "behind Cloudflare") wraps onto lines of its own instead of running off a phone —
   measured at 390px on the demo box, the second chip was cut at the card's edge */
.kv.kv-wrap { flex-wrap: wrap; }
.kv-vals { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 6px; min-width: 0; max-width: 100%; }
.kv-vals .mono { overflow-wrap: anywhere; }

/* stat tiles (database overview) */
.stat-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 12px; margin-bottom: 14px;
}
.stat {
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px 14px; min-width: 0;
}
.stat .v { font-size: 19px; font-weight: 700; letter-spacing: -.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stat .k { color: var(--muted); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; margin-top: 3px; }

/* disk page: inline label + big value */
.stat-inline { display: inline-flex; align-items: baseline; gap: 8px; min-width: 0; }
.stat-inline .k { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .06em; }
.stat-inline .v { font-size: 17px; font-weight: 700; white-space: nowrap; }

/* monitoring sparklines */
.mon-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.mon-head h3 { margin: 0; }
.spark { min-width: 0; }
.spark svg {
  width: 100%; height: 56px; display: block;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r-ctl);
}

/* ===========================================================================
   console-look log blocks (dark terminal in BOTH themes)
   ======================================================================== */

.log-pre {
  font: 12.5px/1.6 var(--mono);
  color: var(--console-text);
  background: var(--console-bg);
  border: 1px solid var(--console-line);
  border-radius: 12px;
  padding: 12px 14px;
  max-height: 260px;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
  margin: 0;
}

/* ===========================================================================
   files browser
   ======================================================================== */

.file-name { cursor: pointer; display: inline-flex; align-items: center; gap: 8px; }
.file-name:hover { color: var(--accent); }
.file-name svg { color: var(--muted); flex: 0 0 auto; }
.file-name.dir svg { color: var(--warn); }
/* in the file managers a row's name is a button — it opens, and the keyboard reaches
   it — drawn as the text it always was */
button.file-name {
  border: 0; background: none; padding: 0; margin: 0; max-width: 100%;
  font: inherit; color: inherit; text-align: start; border-radius: 4px;
}
button.file-name:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* toolbar */
.fm-toolbar { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-block-end: 12px; }
.fm-toolbar .btn { display: inline-flex; align-items: center; gap: 6px; }
.fm-toolbar .btn svg { flex: 0 0 auto; opacity: .85; }
.fm-toolbar .sep { width: 1px; align-self: stretch; background: var(--border); margin-inline: 4px; }
/* THE SELECTION BAR: what acts on the selected rows, on a row of its own under the
   toolbar. With nothing selected it holds one line on how to select. ONE ROW EITHER
   WAY — it scrolls sideways and never wraps: a bar that grew on the first click would
   push the list down, and the second click of a double-click would land on the row
   above the one that was meant. */
.fm-selbar {
  flex: 1 0 100%; min-width: 0; min-height: 40px;
  display: flex; align-items: center; gap: 6px;
  padding: 5px 10px; border-radius: var(--r-ctl);
  background: var(--well); overflow-x: auto;
}
.fm-selbar.on { background: color-mix(in srgb, var(--accent) 9%, var(--well)); }
.fm-selbar .hint { margin: 0; font-size: 12.5px; }
.fm-selbar-acts { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; white-space: nowrap; }
.fm-selbar-acts .sep { align-self: center; height: 18px; }

/* breadcrumb path bar: clickable segments; clicking swaps in an editable
   path input (.fm-crumbs input) for jump-to-path */
.fm-crumbs {
  display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
  font: 12.5px/1.5 var(--mono);
  background: var(--well); border: 1px solid var(--border);
  border-radius: var(--r-ctl);
  padding: 8px 12px; margin-block-end: 12px;
  cursor: text; min-height: 36px;
}
.fm-crumbs a { color: var(--accent); cursor: pointer; white-space: nowrap; }
.fm-crumbs .sep { color: var(--muted); }
.fm-crumbs .cur { color: var(--text); font-weight: 600; word-break: break-all; }
.fm-crumbs input {
  flex: 1 1 auto; min-width: 140px; min-height: 0;
  border: 0; background: transparent; padding: 0; border-radius: 0;
  font: inherit; color: var(--text);
}
.fm-crumbs input:focus { outline: none; box-shadow: none; }
.fm-crumbs a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
/* the pencil at the end of the bar: the path, to type */
.fm-crumb-edit {
  margin-inline-start: auto; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 24px; min-height: 24px; padding: 0;
  border: 0; border-radius: 6px; background: none; color: var(--muted); cursor: pointer;
}
.fm-crumb-edit:hover { color: var(--accent); background: var(--accent-soft); }
.fm-crumb-edit:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* THE PROGRESS STRIP of a file action (files.js fmTasks), above the list: what is
   happening, how many of how many (or the time that has passed), the item in hand,
   and a bar where there is a total. The colours of .busy-strip, which says the same
   kind of thing for a job. Red (.err) once a task ended part-way: it then lists what
   was not done, until it is closed. */
.fm-strip {
  margin: 0 0 12px; padding: 9px 12px; border-radius: 10px;
  border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
  background: var(--warn-soft); color: var(--warn-fg);
  font-size: 13px;
}
.fm-strip.err {
  border-color: color-mix(in srgb, var(--err) 45%, transparent);
  background: var(--err-soft); color: var(--err-fg);
}
.fm-strip-row { display: flex; align-items: center; gap: 9px; }
.fm-strip-what { flex: 1 1 auto; min-width: 0; font-weight: 600; overflow-wrap: anywhere; }
.fm-strip-num { flex: 0 0 auto; white-space: nowrap; font-variant-numeric: tabular-nums; }
.fm-strip-row .btn { flex: 0 0 auto; }
.fm-strip-cur {
  margin-top: 4px; font: 12px/1.45 var(--mono); opacity: .85;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fm-strip .meter { margin-top: 8px; background: color-mix(in srgb, var(--warn) 20%, transparent); }
.fm-strip .meter i { background: var(--warn); }
.fm-strip-list { margin: 6px 0 0; padding-inline-start: 18px; }
.fm-strip-list li { overflow-wrap: anywhere; }

/* table extensions (applied alongside .tbl) */
.fm-table tr.sel td { background: color-mix(in srgb, var(--accent) 8%, transparent); }
/* a click anywhere on a row selects it; the row the keyboard is on is marked while
   the list has the keyboard */
.fm-table tbody tr[data-name] { cursor: default; }
.fm-drop .tbl-wrap:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r-ctl); }
.fm-drop .tbl-wrap:focus-within .fm-table tr.cur td { background: color-mix(in srgb, var(--accent) 15%, transparent); }
/* …the row's last cell included: it is pinned with a solid background of its own
   (.tbl td:last-child:has(> .btn), further up), so the tint is mixed onto that surface */
.fm-table tr.sel td.actions:last-child { background: color-mix(in srgb, var(--accent) 8%, var(--surface)); }
.fm-drop .tbl-wrap:focus-within .fm-table tr.cur td.actions:last-child { background: color-mix(in srgb, var(--accent) 15%, var(--surface)); }
.fm-table th.fm-check, .fm-table td.fm-check { width: 32px; padding-inline-end: 0; }
.fm-table td.fm-perms { font-family: var(--mono); font-size: 12px; color: var(--muted); white-space: nowrap; }
.fm-table th.sortable { cursor: pointer; user-select: none; }
.fm-table th.sortable:hover { color: var(--text); }
.fm-table td .file-name { word-break: break-all; }
/* ON A PHONE THE NAME GETS THE ROW. Six columns at 390px left the name ~20px, so
   "wp-admin/" ran one letter per line and every row was ten lines tall (1 Oct 2026
   walk, every website's Files page). Size and permissions step out under 600px —
   both are one tap away in the row's menu — and the date stays on one short line. */
@media (max-width: 600px) {
  .fm-table:has(th.fm-check) th:nth-child(3), .fm-table:has(th.fm-check) td:nth-child(3),
  .fm-table:has(th.fm-check) th:nth-child(5), .fm-table:has(th.fm-check) td:nth-child(5) { display: none; }
  .fm-table:has(th.fm-check) { table-layout: fixed; width: 100%; }
  .fm-table:has(th.fm-check) th:nth-child(4), .fm-table:has(th.fm-check) td:nth-child(4) { width: 96px; font-size: 12px; color: var(--muted); }
  .fm-table:has(th.fm-check) th.actions, .fm-table:has(th.fm-check) td.actions { width: 40px; }
  .fm-table td .file-name { word-break: normal; overflow-wrap: anywhere; }
}
/* pressed with a finger, the name and the selection bar's buttons are 24 px tall like
   every other control (the rule of the 860 px block further up) */
@media (max-width: 860px) {
  button.file-name { min-height: 24px; min-width: 24px; }
  /* the hint is two lines on a phone: the bar is that tall with the actions in it too */
  .fm-selbar { min-height: 48px; }
  .fm-selbar .hint { font-size: 12px; }
}

/* drag-drop upload target (on the table wrap) */
.fm-drop {
  position: relative; border: 1.5px dashed transparent; border-radius: var(--r-ctl);
  transition: border-color var(--tick), background var(--tick);
}
.fm-drop.dragover {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 5%, transparent);
}

/* context menu (right-click / "⋯ Actions") — positioned by JS via left/top */
.fm-ctx {
  position: absolute; z-index: 95; min-width: 190px;
  margin: 0; padding: 6px; list-style: none;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 12px; box-shadow: var(--shadow-2);
}
.fm-ctx li { list-style: none; }
.fm-ctx .ctx-sep { border-top: 1px solid var(--border); margin: 5px 0; }
.fm-ctx button {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 8px 10px; border: 0; border-radius: 8px;
  background: transparent; color: var(--text); font: inherit; font-size: 13px;
  text-align: start; cursor: pointer;
  transition: background var(--tick);
}
.fm-ctx button:hover { background: var(--accent-soft); color: var(--accent); }
.fm-ctx button:disabled { opacity: .5; cursor: not-allowed; background: none; color: var(--text); }
.fm-ctx button.danger { color: var(--err-fg); }
.fm-ctx button.danger:hover { background: var(--err-soft); color: var(--err-fg); }
.fm-ctx button svg { flex: 0 0 auto; opacity: .85; }

/* full-height editor modal (Files) */
.modal.fm-editor-modal { width: 840px; display: flex; flex-direction: column; }
.fm-editor-modal textarea {
  height: 60vh; min-height: 240px; flex: 1;
  font: 12.5px/1.6 var(--mono); white-space: pre; resize: vertical;
  tab-size: 4;
}

/* ===========================================================================
   login (mesh backdrop: accent + warm radial glows over page bg)
   ======================================================================== */

.login-wrap {
  position: fixed; inset: 0; z-index: 200;
  background:
    radial-gradient(46% 38% at 8% 0%, color-mix(in srgb, var(--accent) var(--mesh-a), transparent), transparent 70%),
    radial-gradient(42% 36% at 100% 100%, color-mix(in srgb, var(--warm) var(--mesh-b), transparent), transparent 70%),
    var(--bg);
  /* CENTRED WHEN IT FITS, SCROLLABLE WHEN IT DOES NOT.
     `justify-content: center` alone is a trap on a fixed, full-height box: when
     the content is taller than the viewport, flex centring overflows in BOTH
     directions and the overflow above the container is unreachable — there is no
     scroll position that reveals it. Measured on the two-factor setup screen,
     which is the tallest of these (QR + secret + code form + skip): the heading
     and the brand mark were cut off the top of a 1050px window and could not be
     scrolled to.

     `overflow-y: auto` plus `margin: auto` on the child is the fix that keeps
     both behaviours: the auto margins centre it while there is room, and they
     collapse to zero when there is not, leaving a normal scrollable box. */
  display: flex; flex-direction: column; align-items: center;
  overflow-y: auto;
  padding: 24px;
}
/* The auto margins that do the centring: one above the first child and one below
   the last, so the whole group (card + controls + credit) is centred as a block
   and stays together. They collapse to zero the moment the content is taller
   than the box, which is what turns it into a plain scrollable page. */
.login-wrap > .login-card { margin-top: auto; }
/* the shorthand on .foot-note sets all four margins, so the bottom auto has to
   come after it in specificity as well as in source order */
.login-wrap > .foot-note { margin-top: 20px; margin-bottom: auto; }

/* TWO-FACTOR SETUP IS A WIDER SCREEN, and it has to be allowed to say so.
   370px is right for two fields and a button — it is wrong for a QR code beside
   a 32-character secret, a code field and a skip control. Locked to 370 the QR
   and the secret stacked, every hint wrapped to three lines, and the card grew
   past the window (which is what made the unreachable-overflow bug above
   visible). The enrolment card gets the width its content needs; the sign-in
   card is untouched. */
#enrolCard { width: 560px; }
/* Below that, the QR and the secret stack — which is correct, and .qr-row
   already wraps on its own. */
@media (max-width: 620px) { #enrolCard { width: 370px; } }
.login-card {
  width: 370px; max-width: 100%;
  background: var(--surface); border-radius: var(--r-modal);
  box-shadow: var(--shadow-2);
  /* FLUID PADDING, so there is no size at which the card is either cramped or
     wasteful. The card had a single fixed 34px/30px pair and no responsive rule
     at all: on a 360px phone that left 300px of usable width inside a screen
     already down to 312px, and the inputs went edge to edge with the card. clamp
     scales it with the viewport between the two sizes that are actually right —
     20px on the narrowest phone, 34px from tablet up — and needs no breakpoint. */
  padding: clamp(24px, 5vw, 34px) clamp(20px, 4.5vw, 30px);
  display: flex; flex-direction: column; gap: 15px;
  position: relative;
  animation: rise-in .18s ease-out;
}
/* SHORT WINDOWS, which are not the same question as narrow ones. A phone in
   landscape is 390x844 rotated: 844 wide and ~390 tall, and the card plus its
   footer credit is taller than that — so the whole thing was centred inside a
   viewport it did not fit, and the top of the card (the brand, the hint) was
   simply unreachable. Scroll from the top instead of centring, and tighten the
   vertical rhythm so it mostly fits anyway. */
@media (max-height: 700px) {
  .login-wrap { justify-content: flex-start; overflow-y: auto; padding-top: 24px; padding-bottom: 24px; }
  .login-card { padding-top: 22px; padding-bottom: 22px; gap: 12px; }
  .login-brand { margin-bottom: 2px; }
  .login-icon { width: 42px; height: 42px; }
}
/* NO ORPHAN LAST WORD. Measured: the English hint wrapped to two lines with
   "server." alone on the second, in a 370px card. `text-wrap: pretty` is the
   right tool rather than a shorter sentence or a hand-placed <br>: the browser
   rebalances the lines it actually has, so it fixes the same defect in all
   eight languages — Bengali and German are longer, not shorter — and it cannot
   be broken by a future translation. Browsers without it simply wrap as before. */
.login-card .hint { text-align: center; margin: 0; text-wrap: pretty; }
.login-error { color: var(--err-fg); text-align: center; margin: 0; font-size: 13px; }
/* SIGN-IN, STEP 1 AND STEP 2. Each step is one flex child of the card, so it
   restates the card's own column gap for the fields inside it. */
.login-step { display: flex; flex-direction: column; gap: 15px; }
/* The card is vertically centred, so swapping a two-field step for a one-field
   step would move the logo. core.js loginStep() pins the height and animates to
   the new one; .sizing is on only for that 200ms, because the brand halo is
   drawn outside the card's box and must not be clipped the rest of the time. */
.login-card { transition: height .2s ease; }
.login-card.sizing { overflow: hidden; }
@media (prefers-reduced-motion: reduce) { .login-card { transition: none; } }
/* who is signing in — quiet, but the name itself is not */
.login-as { text-align: center; margin: -4px 0 0; font-size: 13px; color: var(--muted); }
/* ===========================================================================
   SIX-DIGIT CODE FIELD — six slots drawn over ONE input

   Six separate boxes is the right picture: it says how many digits are wanted
   before the reader types anything, and each digit lands somewhere definite. Six
   separate <input>s is the wrong implementation of that picture, and it breaks
   the three things this field is used with most:

     * PASTE. An authenticator shows "123 456" and that is what gets copied. Into
       six inputs, the whole string lands in the first one.
     * AUTOFILL. iOS and Android fill `autocomplete="one-time-code"` from the
       notification, and a password manager fills a TOTP field the same way. Both
       need one field with one value.
     * SELECT-ALL, backspace across the boundary, undo, and the caret. All of it
       is free in one input and all of it has to be re-implemented, badly, across
       six.

   So there is one <input> holding the value, and the slots are drawn behind it:
   a grid of six empty boxes, with the input transparent on top, its own text
   positioned so each character sits in the middle of its slot. Everything the
   browser gives a text field keeps working, and the field still looks like six
   boxes. The geometry lives in two custom properties, so slot size and gap are
   changed in one place and the input's letter-spacing follows automatically.
   ======================================================================== */

.code-field {
  --slot: 46px;            /* one box */
  --slot-gap: 8px;         /* space between boxes */
  position: relative;
  width: calc(6 * var(--slot) + 5 * var(--slot-gap));
  max-width: 100%;
  margin: 2px auto 16px;   /* the card's column gap does not apply inside a form */
  direction: ltr;          /* digits are digits; an RTL page must not reverse them */
}
/* the six drawn boxes */
.code-slots {
  display: grid;
  grid-template-columns: repeat(6, var(--slot));
  gap: var(--slot-gap);
}
.code-slots > span {
  height: calc(var(--slot) * 1.18);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-ctl);
  background: var(--surface);
  transition: border-color var(--tick), box-shadow var(--tick), background var(--tick);
}
/* THE INPUT, laid exactly over the slots.
   Transparent background and no border of its own: the boxes underneath are the
   visible field. Each character is centred in its slot by making the advance
   width of one character equal one slot plus one gap — letter-spacing adds after
   the glyph, so a half-slot indent puts the first digit in the middle of the
   first box and every one after it follows. */
/* THE INPUT IS INVISIBLE, and the digits are drawn in the slots.
   The first build laid a transparent input over the boxes and tried to line its
   text up with them using letter-spacing and text-indent derived from `1ch`. It
   cannot be made to hold: `ch` is the advance width of "0" in the resolved font,
   and `var(--mono)` resolves to a different font on every platform — so the
   digits landed between the boxes, half a box out, with the first one clipped at
   the left edge (measured; visible in the screenshot that prompted this). No
   amount of tuning fixes a measurement the stylesheet cannot know.

   So nothing is aligned to anything: the input keeps the value and the caret and
   is transparent, and each slot paints its own digit from `attr()`, centred by
   the grid that already positions the boxes. There is no geometry left to get
   wrong, and it is exact in every font. */
.code-input {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  margin: 0; padding: 0;
  border: 0; background: transparent; outline: none; box-shadow: none;
  /* the value is present for select-all, paste, autofill and the caret — just
     not painted; `transparent` and not `visibility:hidden`, which would take the
     caret with it */
  color: transparent;
  caret-color: transparent;   /* the slots show focus; a caret here would sit wrong
                                 for the same reason the digits did */
  font-family: var(--mono);
  font-size: 21px;
  /* a text cursor across the whole field, so clicking any box focuses it */
  cursor: text;
}
/* Chrome paints its own colour for an autofilled field; without this the value
   reappears on top of the drawn digits after a one-time-code autofill. */
.code-input:-webkit-autofill,
.code-input:-webkit-autofill:focus {
  -webkit-text-fill-color: transparent;
  transition: background-color 9999s;
}
/* THE DIGIT, painted by its own slot. core.js sets --d1…--d6 on the wrapper on
   every input event, in the same pass that writes data-filled, and each slot
   renders its own from a custom property. Custom properties and not attr(),
   because attr() outside `content` is still not usable and the string form needs
   quoting the JS would have to add — a property holds the quoted string once.
   No positioning, no font metrics, no per-digit DOM. */
.code-slots > span {
  display: flex; align-items: center; justify-content: center;
  font-family: var(--mono);
  font-size: 21px; font-weight: 650; color: var(--text);
  font-variant-numeric: tabular-nums;
  /* the boxes are decoration to a screen reader — the input carries the value */
  user-select: none;
}
.code-slots > span:nth-child(1)::before { content: var(--d1, ''); }
.code-slots > span:nth-child(2)::before { content: var(--d2, ''); }
.code-slots > span:nth-child(3)::before { content: var(--d3, ''); }
.code-slots > span:nth-child(4)::before { content: var(--d4, ''); }
.code-slots > span:nth-child(5)::before { content: var(--d5, ''); }
.code-slots > span:nth-child(6)::before { content: var(--d6, ''); }
/* the caret, drawn as the focused empty box's own blinking bar — the real caret
   is transparent (see above), so this is what tells the reader where they are */
.code-field:focus-within .code-slots > span.at::after {
  content: ''; width: 2px; height: 22px;
  background: var(--accent);
  animation: code-caret 1.06s steps(1, end) infinite;
}
@keyframes code-caret { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .code-field:focus-within .code-slots > span.at::after { animation: none; }
}
/* FILLED SLOTS read as done: a firmer border and a tinted fill. `data-filled="N"`
   is written on the wrapper by core.js on every input event, so which boxes hold
   a digit is expressed once, in the DOM, and CSS does the rest — no JS ever
   touches a style property here. */
.code-field[data-filled="1"] .code-slots > span:nth-child(-n+1),
.code-field[data-filled="2"] .code-slots > span:nth-child(-n+2),
.code-field[data-filled="3"] .code-slots > span:nth-child(-n+3),
.code-field[data-filled="4"] .code-slots > span:nth-child(-n+4),
.code-field[data-filled="5"] .code-slots > span:nth-child(-n+5),
.code-field[data-filled="6"] .code-slots > span:nth-child(-n+6) {
  border-color: color-mix(in srgb, var(--accent) 42%, var(--border-strong));
  background: color-mix(in srgb, var(--accent) 7%, var(--surface));
}

/* THE RING GOES ON THE BOX THE CARET IS IN — one box, and only while focused.
   The first attempt at this put a ring on box one AND on the next empty box, and
   tried to cancel the first with a later `box-shadow: none` — but the two rules
   had the same specificity, so source order decided and the filled boxes kept a
   ring they had no business showing (measured: digits in 1 and 2, rings on 1, 2
   and 3 at once). One selector per state, `data-filled` choosing the box, and no
   cancelling rule to get the order wrong. */
.code-field:focus-within[data-filled="0"] .code-slots > span:nth-child(1),
.code-field:focus-within[data-filled="1"] .code-slots > span:nth-child(2),
.code-field:focus-within[data-filled="2"] .code-slots > span:nth-child(3),
.code-field:focus-within[data-filled="3"] .code-slots > span:nth-child(4),
.code-field:focus-within[data-filled="4"] .code-slots > span:nth-child(5),
.code-field:focus-within[data-filled="5"] .code-slots > span:nth-child(6) {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
  background: var(--surface);
}
/* Narrow phones: the field must not overflow a 272px card. Shrinking the slot is
   the whole adjustment — every other measurement is derived from it. */
@media (max-width: 400px) {
  .code-field { --slot: 38px; --slot-gap: 6px; }
  .code-input { font-size: 18px; }
}
/* brand block: icon above the product name, soft gradient halo behind */
.login-brand {
  position: relative; display: flex; flex-direction: column; align-items: center;
  gap: 10px; padding: 4px 0 2px;
}
.login-glow {
  position: absolute; top: -16px; left: 50%; transform: translateX(-50%);
  width: 190px; height: 95px; border-radius: 50%;
  background: var(--grad); filter: blur(44px); opacity: .14; pointer-events: none;
}
:root[data-theme="dark"] .login-glow { opacity: .2; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .login-glow { opacity: .2; }
}
.login-icon { width: 52px; height: 52px; position: relative; }
.login-title { font-size: 20px; font-weight: 700; letter-spacing: -.01em; position: relative; }

/* ===========================================================================
   pre-auth tools (language + theme, before signing in)

   ONE bar serving three overlays — login (#login, z-index 200), forced
   two-factor enrolment (same wrap) and the first-run wizard (.wz / .setup-wrap,
   150). It sits above all of them (201) because it is chrome for whichever one
   is showing, not part of any single card, and because that is what lets it
   survive the wizard rebuilding its card on every step.

   It is display:none whenever the app shell is up: the signed-in header carries
   the same two controls, and drawing both would put two language flags on one
   screen. The rule keys on #shell rather than on a class the JS has to
   remember to set — the shell's `hidden` attribute is already the panel's one
   source of truth for "is the visitor in".
   ======================================================================== */

/* WHERE THEY GO — IN THE LAYOUT, not pinned to the window.

   The first attempt fixed them to the viewport's top-right corner, and that was
   wrong twice over. On the login screen the card is 370px, vertically centred,
   and the rest of the page is empty — so two controls in the far corner read as
   something dropped on the page, as far from where the visitor is looking as it
   is possible to get. And being `position: fixed`, they could not be responsive:
   every screen size needed another hand-written offset, and each one was a guess
   that a different window height broke.

   So on the login and two-factor screens the bar is a normal child of
   `.login-wrap`, which is already a centred flex column: it sits under the card,
   above the footer credit, in the card's own 370px column — and it then follows
   the card at every window size with no media query deciding where it goes. The
   wizard keeps a pinned top-right, because its 860px shell is top-aligned and
   genuinely has a corner to align to.

   `#preAuthTools` lives at the top of <body> in index.html, so it is moved into
   the login wrap by the ONE line in core.js that already knows which screen is
   showing — not copied into three places in the markup. */

/* THE BOOT GATE — see the note in theme-boot.js. The whole document is held
   back until core.js knows which screen to show, so a reload is one paint
   instead of three. `visibility`, not `display`, so layout is already computed
   when it is revealed and nothing reflows on the way in; the background is
   painted so the hold looks like the page loading rather than a white flash. */
html.booting { background: var(--bg); }
html.booting body > * { visibility: hidden; }

.pre-auth-tools {
  display: flex; align-items: center; gap: 8px;
  /* LOGICAL DIRECTION IS WRONG FOR THIS ROW. flex mirrors the ORDER of its
     children in RTL, so on Arabic the theme button came first and the language
     button moved (measured on the live box). The one control a reader reaches for
     BECAUSE the page is in the wrong language must not change places when the
     language changes. */
  direction: ltr;
}
/* Signed in -> the header owns these controls. Keys on #shell's `hidden`
   attribute, already the panel's one source of truth for "is the visitor in". */
body:has(#shell:not([hidden])) .pre-auth-tools { display: none; }

/* Each button carries its own surface: these screens have no card behind the
   controls. A real token and a real border, not a translucent mix of --surface —
   in dark mode --surface is the card colour over a near-black page, so mixing it
   toward transparent lands it ON the page (measured invisible in the dark
   render, only the border left). */
.pre-auth-tools .icon-btn {
  background: var(--surface);
  box-shadow: 0 1px 2px rgba(16, 19, 33, .05), 0 6px 18px rgba(16, 19, 33, .06);
}
.pre-auth-tools .icon-btn:hover { background: var(--surface-2); }
.pre-auth-tools .icon-btn[aria-expanded="true"] {
  background: var(--accent-soft); border-color: var(--accent); color: var(--accent);
}
.pre-auth-tools .lang-pop { z-index: 202; direction: inherit; }

/* ---- in the login / two-factor column ----
   BETWEEN the card and the footer credit, not after both. The bar is appended to
   the wrap, so in DOM order it lands last — below the credit line, which read as
   an afterthought stuck to the bottom of the page. `order` puts it back where it
   belongs without the JS having to insert it at a particular index (it moves
   between three different parents, and each one would need its own rule). */
.login-wrap .pre-auth-tools { order: 1; margin-top: 20px; }
.login-wrap .foot-note { order: 2; }
/* Down here the popover would open off the bottom of the screen, so it opens
   UPWARD, centred on the control that opened it. */
.login-wrap .pre-auth-tools .lang-pop {
  top: auto; bottom: calc(100% + 6px);
  inset-inline-end: auto; inset-inline-start: 50%;
  transform: translateX(-50%);
}

/* ---- wizard: the window corner, clear of the card ----
   This was `top: 32px` right-aligned to the shell's own 860px column, which put
   it exactly on the card's top-right corner — fine while .wz had 32px of top
   padding and the shell was pinned there, and broken the moment the shell moved
   down the page: on the two-factor step the bar sat INSIDE the card, over its
   heading. Measured in the screenshots.

   A control that must not collide with a card of unpredictable height does not
   belong on that card's edge. The window corner is stable whatever the step
   does, and the shell's 860px column leaves room beside it on every width the
   wizard is readable at. */
body:has(#setup:not([hidden])) .pre-auth-tools {
  position: fixed; z-index: 201;
  top: 18px;
  inset-inline-end: 18px;
}
/* HEADROOM FOR THE BAR, always — not only on narrow windows.
   The bar is 34px tall at y=18, so the card must start below 62px or the two
   share that strip. It was only guaranteed under 940px, and a 1280x720 laptop is
   neither narrow nor tall: measured there, the card started at y=32 with the bar
   ending at 52 — a 20px overlap of the card's top edge, on the most ordinary
   screen size there is.

   So the floor is unconditional and the tall-window rule below only ever adds to
   it. One number, one reason: 62px is the bar's own bottom plus a hair. */
body:has(#setup:not([hidden])) .wz { padding-top: 62px; }

@media (max-width: 520px) {
  body:has(#setup:not([hidden])) .pre-auth-tools { top: 12px; }
}

/* Touch: 38px targets. A 34px target is under the 44px both platforms ask for,
   and these two are the controls a visitor uses before they can read anything.
   Keyed on the INPUT, not on a width — a 1280px tablet is touch and a narrow
   desktop window is not. */
@media (hover: none) and (pointer: coarse) {
  .pre-auth-tools .icon-btn { width: 40px; height: 40px; }
}

/* Dark: an opaque raised surface and a shadow a dark backdrop can show. Both
   selectors, because the panel pins dark with [data-theme] AND follows an
   unpinned device through the media query; one rule covers only half the
   visitors. */
:root[data-theme="dark"] .pre-auth-tools .icon-btn {
  background: var(--surface-2); border-color: var(--border-strong);
  box-shadow: 0 2px 8px rgba(0, 0, 0, .35);
}
:root[data-theme="dark"] .pre-auth-tools .icon-btn:hover {
  background: color-mix(in srgb, var(--text) 10%, var(--surface-2));
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .pre-auth-tools .icon-btn {
    background: var(--surface-2); border-color: var(--border-strong);
    box-shadow: 0 2px 8px rgba(0, 0, 0, .35);
  }
  :root:not([data-theme="light"]) .pre-auth-tools .icon-btn:hover {
    background: color-mix(in srgb, var(--text) 10%, var(--surface-2));
  }
}

/* ===========================================================================
   drawer (bottom sheet log viewer)
   ======================================================================== */

.drawer {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 90;
  height: 42vh; display: flex; flex-direction: column;
  background: var(--surface);
  border-top: 1px solid var(--border);
  border-radius: 16px 16px 0 0;
  box-shadow: 0 -8px 40px rgba(16, 19, 33, .16);
  overflow: hidden;
  animation: drawer-up .18s ease-out;
}
:root[data-theme="dark"] .drawer { box-shadow: 0 -8px 40px rgba(0, 0, 0, .5); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .drawer { box-shadow: 0 -8px 40px rgba(0, 0, 0, .5); }
}
.drawer.full { height: 88vh; }
.drawer-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 16px; border-bottom: 1px solid var(--border);
}
/* THE TITLE GIVES WAY, NEVER THE BUTTONS. A job's name is as long as it is ("backup full ->
   This server (local disk)") and "Report this", "Expand" and "Close" are as long as their
   language makes them: on a 390 px phone in German "Erweitern" stood at 334..415 and
   "Schließen" at 425..507 — the way to close the log was off the screen (the UI audit,
   --press, 9 Oct 2026). So the title is one line that shrinks and is cut with an ellipsis
   (its whole text is in title="", core.js drawerPrepare), and the chip and the buttons
   keep their size. The title is dir="auto" (index.html): an English name on an Arabic page
   is cut at its end, not at its beginning — and stays at the reading edge of its box. */
.drawer-title {
  font-weight: 650; font-size: 13px;
  flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-align: left;
}
[dir="rtl"] .drawer-title { text-align: right; }
.drawer-bar > .chip, .drawer-bar > .btn { flex: 0 0 auto; }
/* …AND ON A PHONE THE BUTTONS HAVE A ROW OF THEIR OWN. With the title at no width at all
   the chip and the three buttons are 466 px of a 390 px screen in German, so one row
   cannot hold them in any order: the title and the job's state take the first row (the
   title all of what the chip leaves), the spacer ends that row, and the buttons follow at
   the far edge — on a third row too, in a language where three do not fit on one. */
@media (max-width: 860px) {
  .drawer-bar { flex-wrap: wrap; justify-content: flex-end; gap: 4px 10px; }
  .drawer-title { flex: 1 1 0; }
  .drawer-bar > .spacer { flex: 0 0 100%; }
}
.drawer-log {
  flex: 1; margin: 0; padding: 12px 16px;
  overflow: auto; font: 12.5px/1.6 var(--mono);
  white-space: pre-wrap; word-break: break-word;
  color: var(--console-text);
  background: var(--console-bg);
}

/* ===========================================================================
   modals
   ======================================================================== */

.modal-wrap {
  /* ABOVE EVERY OVERLAY, including the ones that are themselves fixed. This was
     100 while .login-wrap is 200 and .wz / .setup-wrap are 150 — so a modal
     opened from the first-run wizard rendered BEHIND it. "Exit setup" looked
     dead: the confirmation was in the DOM, waiting on a click nobody could see.
     A modal is the frontmost thing on screen by definition; toasts (110) sit
     above it because they report what it did. */
  position: fixed; inset: 0; z-index: 260;
  background: var(--overlay);
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
  display: flex; align-items: center; justify-content: center; padding: 20px;
  animation: fade-in .18s ease-out;
}
.modal {
  width: 480px; max-width: 100%; max-height: 88vh; overflow: auto;
  background: var(--surface); border-radius: var(--r-modal);
  padding: 24px;
  box-shadow: var(--shadow-2);
  animation: rise-in .18s ease-out;
}
/* a click on the backdrop does not close a modal: the box nudges instead */
.modal.modal-nudge { animation: modal-nudge .22s ease-out; }
@keyframes modal-nudge { 0%, 100% { transform: scale(1); } 45% { transform: scale(1.018); } }
@media (prefers-reduced-motion: reduce) { .modal.modal-nudge { animation: none; } }
.modal h3 { margin-bottom: 10px; font-size: 16px; }
.modal-body { color: var(--muted); margin: 0 0 14px; font-size: 13px; }
.modal-body b, .modal-body code { color: var(--text); }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
/* A LONG MODAL KEEPS ITS BUTTONS IN VIEW. The box scrolls past 88vh, and a wizard
   step that grew (a deploy key, a branch list, a verdict) pushed Continue below the
   fold — the one control the step exists for. Stuck to the bottom edge, flush with
   the box's own padding, and identical to before when nothing scrolls. */
.modal .modal-actions {
  position: sticky; bottom: -24px; z-index: 1;
  margin-top: 6px; margin-bottom: -24px; padding: 12px 0 24px;
  background: var(--surface);
}
.modal.wide { width: 640px; }
.typed-hint { font-size: 13px; color: var(--muted); margin: 0 0 8px; }
.typed-hint b { color: var(--err-fg); font-family: var(--mono); }

/* ===========================================================================
   toasts (top end corner, colored accent bar per type)
   ======================================================================== */

#toasts {
  /* ABOVE THE MODAL (260). A toast reports what an action did, and the action is
     usually a modal's confirm button — so a toast behind that modal is a message
     nobody reads. It was 110, which sat under the modal the moment the modal was
     raised to 260 for the wizard. */
  position: fixed; top: 16px; inset-inline-end: 16px; z-index: 300;
  display: flex; flex-direction: column; gap: 10px;
  /* WIDER, because these carry sentences and not words. The panel's own error
     text names a host and says what to do about it ("alllllcom.com is already on
     this project's admin list"); at 400px that wrapped mid-hostname. */
  max-width: min(460px, calc(100vw - 32px));
  /* The stack must not swallow clicks on the page behind it — only the toasts
     themselves are interactive. */
  pointer-events: none;
}
#toasts > * { pointer-events: auto; }
/* ON A PHONE it spans the width and sits below the sticky header, rather than
   clinging to one corner where a 460px card cannot fit anyway. */
@media (max-width: 560px) {
  #toasts {
    top: 12px; inset-inline: 12px; max-width: none;
  }
}
/* A NOTIFICATION HAS TO LOOK LIKE ONE.
   Kevin's report, with a screenshot: "notification shi se dikhta nai" — it did not
   read as an arriving message at all. Three things were wrong and all three were
   about weight, not wording:
     * it wore the SAME surface, border and radius as every card on the page, so it
       looked like part of the layout that happened to be floating;
     * the only mark of a failure was a 20px circle at 13.5px text — an error and a
       success differed by the colour of one small glyph;
     * nothing said WHAT it was. The reader had to infer "this is an error" from
       that glyph, while the sentence itself just named a host.
   So: a tinted ground per kind, a heavier shadow that lifts it off the page, a
   4px accent edge, and a one-word heading above the message. */
.toast {
  display: grid;
  grid-template-columns: auto 1fr auto;
  grid-template-rows: auto auto;
  column-gap: 11px; row-gap: 2px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-inline-start: 4px solid var(--accent);
  color: var(--text); font-size: 13.5px; line-height: 1.5;
  border-radius: 12px; padding: 13px 14px;
  /* Deeper and wider than a card's. A notification arrives ON TOP of the page and
     the shadow is what says so. */
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, .12), 0 4px 6px -4px rgba(0, 0, 0, .1), 0 20px 40px -12px rgba(0, 0, 0, .18);
  animation: toast-in .22s ease-out;
}
.toast.out { opacity: 0; translate: 0 -6px; transition: opacity .18s ease-in, translate .18s ease-in; }
/* the verdict icon: bigger, and on the tint of its own kind, so the kind reads
   from across the screen rather than at reading distance */
.toast-ico {
  grid-row: 1 / span 2; align-self: start;
  flex: 0 0 auto; width: 26px; height: 26px; margin-top: 1px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent-soft); color: var(--accent);
}
.toast-ico svg { width: 15px; height: 15px; }
/* THE ONE-WORD VERDICT. "Error" over the sentence is the difference between a
   reader deducing what happened and being told. */
.toast-title {
  grid-column: 2; grid-row: 1;
  font-weight: 650; font-size: 13px; letter-spacing: .01em;
  color: var(--text);
}
.toast-msg { grid-column: 2; grid-row: 2; min-width: 0; overflow-wrap: break-word; color: var(--muted); }
/* A toast with no heading (the plain informational kind) keeps its message on the
   first row rather than leaving a gap where the title would have been. */
.toast:not(:has(.toast-title)) .toast-msg { grid-row: 1 / span 2; align-self: center; }
.toast-x {
  grid-column: 3; grid-row: 1;
  flex: 0 0 auto; border: 0; background: none; padding: 3px;
  margin-block-start: -3px; margin-inline-end: -3px;
  color: var(--muted); cursor: pointer; border-radius: 6px; line-height: 0;
}
.toast-x:hover { color: var(--text); background: var(--surface-2); }
/* the one way on a notice can carry ("Review"): under the sentence, in its column */
.toast-act { grid-column: 2; grid-row: 3; justify-self: start; margin-top: 8px; }
.toast-x svg { width: 14px; height: 14px; }
/* THE GROUND IS TINTED PER KIND. This is the change that stops a toast reading as
   a floating card: the whole surface says what happened, not one small glyph on it.
   The tint is the same --*-soft token the rest of the panel uses for this, mixed
   toward the surface so body text keeps its contrast — an error is unmistakably an
   error without becoming a red box that is hard to read. */
.toast.ok {
  border-inline-start-color: var(--ok);
  background: color-mix(in srgb, var(--ok-soft) 55%, var(--surface));
  border-color: color-mix(in srgb, var(--ok) 22%, var(--border));
}
.toast.ok .toast-ico { background: var(--ok-soft, var(--accent-soft)); color: var(--ok-fg, var(--ok)); }
.toast.ok .toast-title { color: var(--ok-fg, var(--ok)); }
.toast.err {
  border-inline-start-color: var(--err);
  background: color-mix(in srgb, var(--err-soft) 60%, var(--surface));
  border-color: color-mix(in srgb, var(--err) 24%, var(--border));
}
.toast.err .toast-ico { background: var(--err-soft, transparent); color: var(--err-fg, var(--err)); }
.toast.err .toast-title { color: var(--err-fg, var(--err)); }
.toast.warn {
  border-inline-start-color: var(--warn);
  background: color-mix(in srgb, var(--warn-soft) 55%, var(--surface));
  border-color: color-mix(in srgb, var(--warn) 22%, var(--border));
}
.toast.warn .toast-ico { background: var(--warn-soft, transparent); color: var(--warn-fg, var(--warn)); }
.toast.warn .toast-title { color: var(--warn-fg, var(--warn)); }
/* AN ERROR STAYS UNTIL IT IS READ OR DISMISSED — see toast() in core.js, which
   gives it no timer at all. This is the visual half of that decision: a message
   that persists needs its dismiss control to be obvious. */
.toast.err .toast-x { color: var(--err-fg, var(--err)); opacity: .8; }
.toast.err .toast-x:hover { opacity: 1; background: color-mix(in srgb, var(--err) 12%, transparent); }
/* PRESSED WITH A FINGER IT IS 24 PX EITHER WAY, like every other control (the rule of the
   860 px block further up): it was 20 × 23, on every notice of every page (the UI audit,
   --press on a phone, 9 Oct 2026). The padding grows and the margins take the same back,
   so the notice is laid out as it was — the sentence's column is not a pixel narrower —
   and at a desk's width nothing changes at all. */
@media (max-width: 860px) {
  .toast-x { min-width: 24px; min-height: 24px; padding: 5px; margin-block-start: -5px; margin-inline: -2px -5px; }
}
/* Reduced motion: no slide-in, and no fade-translate on the way out. */
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
  .toast.out { transition: opacity .12s linear; translate: none; }
}

/* ===========================================================================
   misc states
   ======================================================================== */

.loading { color: var(--muted); padding: 34px 0; text-align: center; }
.empty { color: var(--muted); font-size: 13px; padding: 8px 0; }
.empty-inline { color: var(--muted); }
.step-list { margin: 0; padding-inline-start: 20px; color: var(--muted); font-size: 13px; }
.step-list li { margin-bottom: 9px; }
.step-list b { color: var(--text); }
/* an unadorned list (self-heal reports): the class always said "plain" */
ul.plain { list-style: none; margin: 8px 0 0; padding: 0; font-size: 13px; }
ul.plain li { margin-bottom: 8px; }

/* loading skeletons */
.skel { pointer-events: none; }
.skel-bar {
  height: 13px; border-radius: 7px; margin: 13px 0;
  background: linear-gradient(90deg, var(--well) 25%, var(--border) 50%, var(--well) 75%);
  background-size: 200% 100%;
  animation: skel-slide 1.2s ease-in-out infinite;
}
@keyframes skel-slide { from { background-position: 200% 0; } to { background-position: -200% 0; } }

/* ===========================================================================
   health page + verdict banner
   ======================================================================== */

.health-banner { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; border: 1px solid transparent; }
.health-banner.ok      { border-color: color-mix(in srgb, var(--ok) 38%, transparent);   background: color-mix(in srgb, var(--ok-soft) 40%, var(--surface)); }
.health-banner.warnish { border-color: color-mix(in srgb, var(--warn) 42%, transparent); background: color-mix(in srgb, var(--warn-soft) 40%, var(--surface)); }
.health-banner.bad     { border-color: color-mix(in srgb, var(--err) 42%, transparent);  background: color-mix(in srgb, var(--err-soft) 40%, var(--surface)); }
.health-banner.info    { border-color: color-mix(in srgb, var(--accent) 38%, transparent); background: color-mix(in srgb, var(--accent-soft) 40%, var(--surface)); }
.health-light { width: 14px; height: 14px; border-radius: 50%; flex: 0 0 auto; }
.health-banner.ok .health-light      { background: var(--ok);   box-shadow: 0 0 12px color-mix(in srgb, var(--ok) 70%, transparent); }
.health-banner.warnish .health-light { background: var(--warn); box-shadow: 0 0 12px color-mix(in srgb, var(--warn) 70%, transparent); }
.health-banner.bad .health-light     { background: var(--err);  box-shadow: 0 0 12px color-mix(in srgb, var(--err) 70%, transparent); }
.health-banner.info .health-light    { background: var(--accent); box-shadow: 0 0 12px color-mix(in srgb, var(--accent) 60%, transparent); }
.health-verdict { font-size: 16px; font-weight: 700; letter-spacing: -.01em; }
.health-banner .hint { margin: 2px 0 0; }

.health-row { padding: 12px 0; border-bottom: 1px dashed var(--border); }
.health-row:last-child { border-bottom: none; padding-bottom: 0; }
/* wraps, and long paths break anywhere: a finding's chip beside its title, or a
   path like /var/www/<proj>/react/next.config.js, must not widen a phone page */
.health-row-head { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; font-weight: 600; font-size: 13px; }
.health-detail { color: var(--muted); font-size: 13px; margin: 3px 0 0; overflow-wrap: anywhere; }
.health-fix { font-size: 13px; margin: 5px 0 0; overflow-wrap: anywhere; }
.health-fix::before { content: "→ "; color: var(--accent); }

/* SixChecker: the "Fix this from the panel" button.
   Right-aligned on its own line, and `.primary` in the markup, because the
   previous drawing (a bare grey `.btn sm`, left-aligned directly under a
   paragraph of text) did not read as a control — it looked like part of the
   sentence above it. On a narrow screen it goes full width rather than sitting
   in the middle of nowhere. */
.pf-fix-row { justify-content: flex-end; margin-top: 10px; }
@media (max-width: 560px) {
  .pf-fix-row { justify-content: stretch; }
  .pf-fix-row > .btn { flex: 1; }
}
/* A row measured as already fixed: still legible, visibly no longer the point
   of the page. The chip carries the meaning; this only lowers the emphasis. */
.health-row.pf-fixed .health-row-head > span:not(.chip) { font-weight: 600; opacity: 0.75; }
.health-row.pf-fixed .health-detail { opacity: 0.75; }

/* verdict banner: text column + the signal points it was made of */
.vd-text { flex: 1 1 260px; min-width: 0; }
/* max-width, not flex-shrink alone: a row of three buttons wanted 391px inside a
   358px phone card (SixChecker's verdict, demo box 30 Sep 2026) and widened the
   whole page. Capped at its card, the .btn-row wraps its buttons instead. */
.vd-actions { flex: 0 1 auto; max-width: 100%; }
.vd-points { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.vd-point {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; line-height: 1.3; padding: 3.5px 10px;
  border-radius: 999px;
}
.vd-point b { font-weight: 800; font-size: 11px; }
.vd-point.ok   { color: var(--ok-fg);   background: var(--ok-soft); }
.vd-point.warn { color: var(--warn-fg); background: var(--warn-soft); }
.vd-point.bad  { color: var(--err-fg);  background: var(--err-soft); }
.vd-point.off  { color: var(--muted);   background: color-mix(in srgb, var(--muted) 10%, transparent); }

/* an action and the cost of pressing it, side by side */
.act-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 12px 0; }
.act-row + .act-row { border-top: 1px dashed var(--border); }
.act-btn { flex: 0 0 auto; }
.act-impact { margin: 0; flex: 1 1 240px; min-width: 0; font-size: 12.5px; color: var(--muted); }
.act-row.danger .act-impact { color: var(--warn-fg); }

/* a term the rest of the world uses, with its meaning spelled out next to it */
.say { color: var(--muted); font-size: 12.5px; }
.say b { color: var(--text); font-weight: 600; }

/* ===========================================================================
   auto-tune / websocket helpers
   ======================================================================== */

.tune-changed { color: var(--accent); font-weight: 700; }
.sep-line { border: none; border-top: 1px solid var(--border); margin: 16px 0; }
.sub-h { font-size: 13px; margin: 0 0 4px; font-weight: 650; }
.ws-warn {
  color: var(--warn-fg); font-size: 13px;
  background: var(--warn-soft);
  border: 1px solid color-mix(in srgb, var(--warn) 26%, transparent);
  border-radius: var(--r-ctl); padding: 9px 12px; margin: 10px 0 0;
}

/* ===========================================================================
   setup wizard (existing full-screen flow) + modal wizards
   ======================================================================== */

.setup-wrap {
  position: fixed; inset: 0; z-index: 150;
  background:
    radial-gradient(46% 38% at 8% 0%, color-mix(in srgb, var(--accent) var(--mesh-a), transparent), transparent 70%),
    radial-gradient(42% 36% at 100% 100%, color-mix(in srgb, var(--warm) var(--mesh-b), transparent), transparent 70%),
    var(--bg);
  overflow-y: auto;
  display: flex; justify-content: center;
  padding: 36px 18px 64px;
}

/* horizontal step bar (wizBarHtml in core.js) */
.wiz-progress {
  display: flex; align-items: flex-start; justify-content: space-between;
  margin: 0 0 22px; gap: 4px;
}
.wiz-node {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  flex: 1; min-width: 0; position: relative;
}
.wiz-node::before {
  content: ""; position: absolute; top: 13px; right: 50%; width: 100%; height: 2px;
  background: var(--border);
}
.wiz-node:first-child::before { display: none; }
.wiz-node.done::before { background: color-mix(in srgb, var(--accent) 45%, var(--border)); }
.wiz-dot {
  width: 26px; height: 26px; border-radius: 50%; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700;
  background: var(--surface); border: 1px solid var(--border-strong); color: var(--muted);
  transition: background var(--tick), border-color var(--tick), color var(--tick);
}
.wiz-node.cur .wiz-dot {
  border-color: var(--accent); color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
  animation: ring-pulse 2.2s ease-out infinite;
}
.wiz-node.done .wiz-dot { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
.wiz-name { font-size: 11px; color: var(--muted); text-align: center; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wiz-node.cur .wiz-name { color: var(--text); font-weight: 600; }
.wiz-node.done .wiz-name { color: var(--accent); }

.wiz-done-note { color: var(--ok-fg); font-size: 13px; margin: 4px 0 12px; font-weight: 600; }

/* choice / type-picker cards inside wizards */
.type-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; }
.type-card {
  display: flex; flex-direction: column; gap: 4px; text-align: start;
  background: var(--surface); border: 1px solid var(--border-strong); border-radius: 12px;
  padding: 14px 16px; cursor: pointer; color: var(--text); font: 13px/1.5 var(--font);
  transition: border-color var(--tick), background var(--tick), box-shadow var(--tick);
}
.type-card:hover { border-color: var(--accent); }
.type-card:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.type-card b { font-size: 13px; }
.chan-cards .chan-ver { font-size: 12px; color: var(--muted); }
.chan-cards .type-card.off { opacity: .6; cursor: not-allowed; }
.chan-cards .type-card.off:hover { border-color: var(--border); }
.chan-cards .chan-why { color: var(--warn-fg); }
.type-card span { color: var(--muted); font-size: 12px; }
.type-card.sel {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent-soft) 55%, var(--surface));
  box-shadow: 0 0 0 1px var(--accent);
}
.type-card b .chip { margin-inline-start: 6px; }

/* copyable value row (secrets, urls) */
.copy-row { display: flex; gap: 6px; margin: 6px 0; }
/* The value has to be readable WHOLE, not just present. A TOTP secret is 32
   characters and at 12px monospace it needed 255px inside a 250px box — five
   pixels of overflow, which is exactly enough to hide the last character behind
   the edge and let someone paste 31 of 32 into their authenticator and be told
   the code is wrong. min-width:0 lets the field shrink inside the flex row
   instead of pushing the Copy button, and the size steps down just enough to
   fit; below that it scrolls, which is honest, rather than silently clipping. */
.copy-row .input { font-family: var(--mono); font-size: 11.5px; min-width: 0; letter-spacing: -.01em; }
/* A hand-off block is several lines (address / username / password) and is held
   in a <textarea>, because an <input> drops newlines and hands the reader one
   run-together string. It is still a copy field, not an editor: no resize
   handle, no scrollbar chrome to fight with, the same monospace as the
   single-line row, and the Copy button stays top-aligned beside it. */
.copy-row-multi { align-items: flex-start; }
.copy-row-multi textarea.input {
  resize: none; overflow: hidden; line-height: 1.55;
  padding-block: 8px; white-space: pre; overflow-x: auto;
}
.secret-box {
  border: 1px solid color-mix(in srgb, var(--warn) 34%, transparent);
  background: var(--warn-soft);
  border-radius: 12px; padding: 13px 15px; margin: 14px 0;
}
.secret-box .sb-title { color: var(--warn-fg); font-weight: 700; font-size: 13px; margin: 0 0 6px; }
.secret-box p { font-size: 13px; color: var(--text); margin: 6px 0; }

/* numbered mini-steps (websocket wizard & instructions) */
.mini-step { display: flex; gap: 12px; padding: 12px 0; border-bottom: 1px dashed var(--border); }
.mini-step:last-child { border-bottom: none; padding-bottom: 0; }
.mini-step > div { flex: 1; min-width: 0; }
.step-num {
  flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700; color: var(--accent);
  background: var(--accent-soft);
}
.mini-step .hint { margin-top: 2px; }

/* ===========================================================================
   projects page + per-project setup checklist
   ======================================================================== */

/* ONE PROJECT MUST NOT BECOME A FULL-WIDTH BANNER (Kevin, 12 Sep).
   The page used the shared `.cols2` (auto-FIT, minmax(330px, 1fr)), and auto-fit
   collapses the empty tracks and stretches what is left — so a box with one
   project drew a single card 1100px wide holding five short rows, and two cards
   each ~550px. auto-FILL keeps the tracks, and the 460px ceiling stops a card
   growing past the width its own content wants; `justify-content: start` keeps
   them left rather than centred in the leftover space.
   `.cols2` itself is untouched: six other pages use it and none of them has this
   problem. */
/* TWO COLUMNS AT MOST, AND THE COUNT IS A DECISION RATHER THAN AN OUTCOME
   (Kevin, 12 Sep: "only 2 card show hone chaiye the").
   auto-fit was the wrong tool twice over. It fits as many tracks as the row can
   hold, so the column count is whatever the viewport happens to allow — measured at
   1440px: three 364px cards, and at 364px a card's own rows start wrapping (the
   domain rows went from 38px to 59px), so the squeeze that auto-fit produced is
   also what broke the inside of the card. Before that, capping the track fixed the
   one-card banner and left two cards ending 190px short of the page's other cards.
   An explicit ladder instead: one column until there is genuinely room for two
   comfortable ones, then exactly two, never three. 1240px is where the two-column
   arithmetic works out — the page's content area is the viewport less the ~232px
   sidebar and its gutters, so two tracks land at ~475px each, wide enough that a
   domain and its https chip stay on one line. */
.proj-grid {
  display: grid; gap: 18px; margin-bottom: 18px;
  grid-template-columns: minmax(0, 1fr);
  /* THE CARDS OF A ROW ARE ONE HEIGHT (9 Oct 2026), and this reverses an earlier
     answer on purpose. `start` was put here because a project with nothing set up
     yet — two short lines and a progress bar — was pulled to the height of the
     finished card beside it and ended in ~150px of empty box. That was the content's
     fault, not the stretch's: every card now draws the same three rows (pages/
     projects.js), its staging sites fold into one line, and the foot is pinned to
     the bottom edge below — so a stretched card ends in its button, not in a hole,
     and two cards side by side no longer end at two different heights. */
  align-items: stretch;
}
/* …the card a column, its foot on the bottom edge. Only on this grid: a .proj-card
   elsewhere (a staging site's own card) keeps its own flow. */
.proj-grid > .proj-card { display: flex; flex-direction: column; }
.proj-grid > .proj-card > .proj-card-foot { margin-top: auto; padding-top: 16px; }
/* "Staging sites (2)" — one line that opens the rows. The rows bring their own
   "Staging sites" label, which the line above them already is. */
.proj-copies { margin-top: 10px; font-size: 13px; }
.proj-copies > summary { cursor: pointer; color: var(--muted); padding: 4px 0; }
.proj-copies > summary:hover { color: var(--text); }
.proj-copies > .tc-rows { margin-top: 2px !important; }
.proj-copies > .tc-rows > .hint { display: none; }
/* The Apps row of a 6amMart card: the label keeps its place and the chips wrap beside it
   (a .kv-wrap row sent all of them under the label — three lines for one fact). The
   customer website's address and its https chip are one piece, so they wrap together. */
.proj-apps-site { display: inline-flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 6px; min-width: 0; max-width: 100%; }
@media (min-width: 1240px) {
  .proj-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* THE CAP IS ON THE CARD, NOT ON THE TRACK, and that distinction is the whole
   trick. Capping the TRACK (the first attempt) fixed the one-project banner and
   introduced a new fault: two 460px cards under a 1128px page left 190px of dead
   space, so the cards ended in a ragged edge that no longer lined up with the
   banner above them or the deleted shelf below — measured, 938px against 1128px.
   Auto-fit tracks share the row, so two cards fill it and three fill it; the
   max-width only bites when there is ONE card and its track is the whole row,
   which is the case Kevin reported. */
.proj-grid > .card { margin-bottom: 0; max-width: 560px; }
/* One column below 360px: a 320px minimum plus the page gutter overflows a small
   phone, and a horizontally scrolling page is the one thing responsive must never
   produce. minmax(0, …), never a bare 1fr: a bare 1fr is as wide as the widest thing in
   any card, and on a 390 px phone in German that was a chip of one sentence — every card
   on Projects 395 px wide, the page with them (the UI audit on a test server, 7 Oct 2026). */
@media (max-width: 400px) { .proj-grid { grid-template-columns: minmax(0, 1fr); } }

.proj-card-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; flex-wrap: wrap; }
.proj-card-head h3 { margin: 0; }
/* a card's status chip can be a sentence ("Partly done — 2 projects and 2 websites did not
   move"): at 390px it ran past the card's edge on one line (walk of 7 Oct 2026). In a
   card's head wherever that head is used (Migration's cards are plain cards with this
   head), and anywhere on a project card: a website's is in its Status row ("moved here —
   address not pointed here yet"). */
.proj-card-head .chip, .proj-card .chip { white-space: normal; max-width: 100%; min-width: 0; }
/* THE TWO ACTIONS ARE ONE ITEM, so they wrap together or not at all. Measured at
   390px on a card whose name is long ("Untouched 6amMart v4.1"): the head is a
   wrapping flex row, so `rename…` stayed on the first line and `delete…` dropped
   alone onto the second, leaving a stray word under the heading. As a nested flex
   group the pair moves as a unit, and `margin-inline-start:auto` keeps them right
   when the row does not need to wrap. */
.proj-card-acts { display: flex; align-items: baseline; gap: 10px; margin-inline-start: auto; }

/* NARROW: THE ROWS STACK INSTEAD OF WRAPPING RAGGEDLY.
   .kv is `justify-content: space-between`, so on a phone the long rows broke
   unevenly — measured at 390px: three rows 38px tall and the two domain rows 59px,
   because the https chip dropped under the address while its label stayed put. Five
   rows of two different heights with only some of them broken reads as a layout
   accident, which is what it was.
   Below 560px each row becomes label-then-value, left-aligned: every row the same
   shape, the address on its own line with room for it, and the chip beside the value
   it belongs to. Scoped to .proj-card so the .kv used by other pages is untouched. */
@media (max-width: 560px) {
  /* WRAP ONLY WHERE IT IS NEEDED. Forcing every row to stack (label above value)
     made them uniform and made the card much taller — measured: all five rows ~58px
     instead of three at 38px, and a row like "Admin app / installed" spent a whole
     line on a two-word value that always fits.
     So the row stays a flex line and is allowed to wrap: short pairs keep one line,
     and only a long address plus its chip moves down — where it lands left-aligned
     under its label rather than pushed to the right edge, which is what made the
     ragged version look accidental. */
  .proj-card .kv { flex-wrap: wrap; justify-content: flex-start; gap: 2px 10px; }
  .proj-card .kv .k { flex: 0 0 auto; }
  .proj-card .kv > *:not(.k) { margin-inline-start: 0; }
  /* …and the Apps row's chips start where every other value on a phone starts: under
     their label, from the reading edge — not pushed to the far one. */
  .proj-card .kv-vals, .proj-card .proj-apps-site { justify-content: flex-start; }
  /* A STAGING SITE'S ROW CARRIES ITS MARKS IN ITS LABEL: the short name, the owner's
     label and up to three chips ("stopped part-way", "Not started", "sends email"). The
     rule above keeps a label at its own width, which is right for a word and wrong for
     this: on a phone in German the row was 443px on a 390px screen (the UI audit, 8 Oct
     2026) and pushed the page sideways. So this label may take the whole line and break
     between its parts. Written AFTER the rule it overrides — above it, it never applies. */
  .proj-card .kv.tc-row .k { flex: 1 1 100%; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; }
}
/* THE WHOLE NAME IS THE WAY IN (Kevin, 12 Sep). A card's heading that navigates
   has to look like it does — inheriting the h3's colour made it a link nobody
   could see. It takes the accent on hover and carries a focus ring, so keyboard
   and pointer both get the same affordance. */
.proj-card-name { color: inherit; text-decoration: none; }
.proj-card-name:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
.proj-card-name:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
/* WHICH APPLICATION THIS PROJECT RUNS — its own line under the name, so the badge
   never competes with the slug chip for the reader's eye in the head row.
   -8px pulls it under the head's 12px bottom margin: the two lines belong
   together, and the head's margin is what separates the block from the rows. */
/* WHICH APPLICATION THIS PROJECT RUNS — its own line under the name, so it never
   competes with the slug chip in the head row.
   NOT a .badge: the first attempt used one, and a grey chip beside another grey chip
   reads as more metadata — which is exactly the "njr nai jaa rahi" Kevin reported.
   The script name carries the weight and the version sits next to it in mono, with a
   tinted rule to its left tying the pair to the heading above. */
/* The "where does this domain live" verdict inside the add-address dialog. Spaced
   from the field above it, and only when it has content: the box is in the markup
   from the start and stays empty until detect answers, so an unconditional margin
   would leave a dead gap under the input. */
.ds-box:not(:empty) { margin-top: 12px; }
.ds-box .verdict { margin: 0; }
.ds-box .choice-row { margin-top: 10px; }

.proj-card-app { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0 0 14px; }
.proj-card-app-name {
  font-size: 13.5px; font-weight: 650; color: var(--accent);
  padding-inline-start: 9px; border-inline-start: 3px solid var(--accent);
  line-height: 1.25;
}
.proj-card-app-ver {
  font-size: 12.5px; font-weight: 600; color: var(--accent);
  background: var(--accent-soft); border-radius: 999px; padding: 2px 8px;
}
.proj-card-app .hint { font-size: 12px; }
/* THE ROWS ARE THE CARD'S BODY, and the actions are not another row of them.
   Six cards each ended in a bare bordered "Open" that read as a fifth key/value
   line rather than as the way in, which is what Kevin could not make sense of —
   nothing marks it as the card's action. A rule above the actions closes the list
   of facts, and Open is the one filled control inside the card. */
/* NO BORDER OF ITS OWN, and that is a fix rather than a simplification. The rows
   above are .kv, each of which already draws a bottom rule — so a border-top here
   put TWO horizontal lines 14px apart at the foot of every card (measured: last row
   border 1px, foot border 1px, 14px between). The last row's own rule is the one
   that closes the list; the foot just needs air above it. This also holds for the
   cards that have no rows at all (a project with nothing set up yet) and for the
   ones that end in a progress bar or a database warning — none of those get a
   stray line under nothing. */
.proj-card-foot {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 16px;
}
.proj-card-foot .spacer { flex: 1 1 auto; }
/* QUIET UNTIL REACHED FOR. Both of these were painted in the error colour, so the
   loudest thing on a project card was "delete…" — and one of the two is a rename,
   which is not destructive at all. Muted by default, and only the destructive one
   turns red, at the moment the pointer is on it. */
.proj-del { font-size: 12px; color: var(--muted); cursor: pointer; }
.proj-del:hover { text-decoration: underline; color: var(--text); }
.proj-del[data-del-proj]:hover { color: var(--err-fg); }

/* ===== 1.5.0 UI round: EVERY KIND OF PROJECT, one frame =====================
   The six kinds (6amMart, static, Node.js, PHP, WordPress, Create with AI) each
   carry a mark in a tinted tile — on the Projects grid, in the empty page's hero,
   in the create dialog and on Home. The tints are the status/accent tokens, which
   both themes already define, so dark mode needs no second list. */
.kind-ico {
  --kind: var(--accent);
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: 36px; height: 36px; border-radius: 10px;
  color: var(--kind);
  background: color-mix(in srgb, var(--kind) 13%, var(--surface));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--kind) 24%, transparent);
}
.kind-ico.sm { width: 28px; height: 28px; border-radius: 8px; }
.kind-ico.kind-6ammart { --kind: var(--warn); }
.kind-ico.kind-static { --kind: var(--muted); }
.kind-ico.kind-node { --kind: var(--ok); }
.kind-ico.kind-php { --kind: var(--accent); }
.kind-ico.kind-wordpress { --kind: var(--info); }
.kind-ico.kind-ai { background: var(--grad); color: var(--on-solid); box-shadow: none; }

/* A kind to press: the hero's six and the dialog's first screen. A <button>, so
   the keyboard reaches every one; the arrow is the "this goes somewhere" cue. */
.kind-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }
.modal .kind-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 560px) { .kind-cards, .modal .kind-cards { grid-template-columns: minmax(0, 1fr); } }
.kind-card {
  display: flex; align-items: flex-start; gap: 12px; width: 100%; text-align: start;
  background: var(--surface); border: 1px solid var(--border-strong); border-radius: 12px;
  padding: 14px; cursor: pointer; color: var(--text); font: 13px/1.5 var(--font);
  transition: border-color var(--tick), box-shadow var(--tick), transform var(--tick);
}
.kind-card:hover { border-color: var(--accent); box-shadow: var(--shadow-1); transform: translateY(-1px); }
.kind-card:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.kind-card-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1 1 auto; }
.kind-card-text b { font-size: 13.5px; font-weight: 650; }
.kind-card-text span { color: var(--muted); font-size: 12.5px; }
.kind-card-go { align-self: center; color: var(--muted); transition: transform var(--tick), color var(--tick); }
.kind-card:hover .kind-card-go { color: var(--accent); transform: translateX(2px); }
[dir="rtl"] .kind-card-go { transform: scaleX(-1); }
[dir="rtl"] .kind-card:hover .kind-card-go { transform: scaleX(-1) translateX(2px); }

/* The empty page: one hero, the kinds inside it — the ONE thing an empty server
   has to say, said once. */
.proj-hero { padding: 28px; }
.proj-hero h2 { font-size: 20px; margin: 0 0 6px; font-weight: 700; }
.proj-hero > .hint { margin: 0 0 20px; max-width: 72ch; }

/* The page's one action, and the search/filter once there are enough projects to
   need them — a row, not a card: it is a toolbar, not content. */
.proj-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 0 0 16px; }
.proj-toolbar .spacer { flex: 1 1 auto; }
.proj-toolbar input[type="search"] { max-width: 280px; }
.proj-filter { display: inline-flex; gap: 6px; }
@media (max-width: 560px) {
  .proj-toolbar .spacer { display: none; }
  .proj-toolbar input[type="search"] { max-width: none; flex: 1 1 100%; }
}

/* A card head that starts with a kind mark lines up on the mark's centre. */
.proj-card-head.with-ico { align-items: center; gap: 10px; }
.proj-card-head.with-ico h3 { font-size: 15px; }
/* ONE LINE OF HEAD ON A PROJECTS CARD, so two cards side by side start their rows at
   the same height. Measured on the beta box at 1280px: "Lakeside Café — order tracker
   (React)" plus its slug pushed "Create test copy" onto a line of its own, and that
   card's rows sat 30px below its neighbour's. Wide enough for two columns, the head no
   longer wraps: the name gives way (an ellipsis — the whole name is its tooltip and the
   Overview's heading), the slug keeps its width up to a third, the links never shrink.
   A phone keeps the wrapping head, where one column has nothing to line up with. */
@media (min-width: 561px) {
  .proj-grid .proj-card-head.with-ico { flex-wrap: nowrap; }
  .proj-grid .proj-card-head.with-ico > h3 { flex: 0 1 auto; min-width: 0; }
  .proj-grid .proj-card-head.with-ico > h3 .proj-card-name {
    display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .proj-grid .proj-card-head.with-ico > .badge {
    flex: 0 0 auto; max-width: 33%;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .proj-grid .proj-card-head.with-ico > .kind-ico { flex: 0 0 auto; }
  .proj-grid .proj-card-head.with-ico > .proj-card-acts { flex: 0 0 auto; white-space: nowrap; }
}
/* A 6amMart create still in the queue: its job exists, its record does not yet. */
.proj-ghost { border-style: dashed; box-shadow: none; }
/* Something is happening to it right now. */
.chip.pulse::before { animation: chip-pulse 1.4s ease-in-out infinite; }
@keyframes chip-pulse { 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) {
  .kind-card, .kind-card-go { transition: none; }
  .kind-card:hover { transform: none; }
  .chip.pulse::before { animation: none; }
}

/* ---- the create dialog (pages/projects.js openCreateProjectWizard) ---- */
.cpw-head { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 16px; }
.cpw-head-text { min-width: 0; }
.cpw-head h3 { margin: 0 0 2px; }
.cpw-head .hint { margin: 0; }
.cpw .field label .label-opt { font-weight: 500; opacity: .8; }
.cpw .field .field-label { display: block; color: var(--muted); font-size: 12.5px; margin-bottom: 6px; font-weight: 600; }
.cpw-slug b { font-family: var(--mono); font-weight: 600; color: var(--text); }
.cpw-why { color: var(--warn-fg); }
.cpw .note.warn-text { color: var(--warn-fg); }
.cpw .linkish {
  background: none; border: 0; padding: 0; margin-inline-start: 4px; cursor: pointer;
  font: inherit; font-weight: 600; color: var(--accent);
}
.cpw .linkish:hover { text-decoration: underline; }
.cpw .linkish:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.cpw .wz-suggest.cpw-suggest { margin: 8px 0 0; }
.cpw .ds-box .verdict + .btn-row { margin-top: 6px; }
.cpw #cpwNoEmail { margin-top: 10px; }
/* no `white-space: nowrap` on these links: "Add a provider under AI → Providers" runs
   to ~45 characters in bn/de/es/fr/pt and pushed the dialog past a 390px phone */
.cpw #cpwNoEmail a, .cpw #cpwAiNote a { font-weight: 600; }
.cpw-keep { margin: 12px 0 0; }
/* the chain's notes are not all errors: "Queued — starts as soon as …" and
   "Continues on Domain & SSL" read as state, only a failure reads red */
.cpw .wz-chain-dot.wait { border-color: var(--warn); border-style: dashed; }
/* no log box until the job has printed something — a queued job has nothing yet */
.cpw .wz-progress:empty { display: none; }
.cpw .wz-chain-dot.wait ~ .wz-chain-main .wz-chain-note { color: var(--warn-fg); }
.cpw .wz-chain-dot.run ~ .wz-chain-main .wz-chain-note,
.cpw .wz-chain-dot.skip ~ .wz-chain-main .wz-chain-note { color: var(--muted); }

/* Two answers, one of them the default: a segmented control, not a select with one
   real option in it. */
.seg {
  display: inline-flex; gap: 3px; padding: 3px; border-radius: 10px;
  background: var(--well); border: 1px solid var(--border);
}
.seg-opt { position: relative; }
.seg-opt input { position: absolute; opacity: 0; pointer-events: none; }
.seg-opt span {
  display: block; padding: 6px 14px; border-radius: 8px; cursor: pointer;
  font-size: 13px; color: var(--muted); transition: background var(--tick), color var(--tick);
}
.seg-opt input:checked + span { background: var(--surface); color: var(--text); font-weight: 600; box-shadow: var(--shadow-knob); }
.seg-opt input:focus-visible + span { box-shadow: 0 0 0 3px var(--accent-soft); }

.pchk { margin-top: 14px; border-top: 1px dashed var(--border); padding-top: 12px; }
.pchk-row { display: flex; gap: 10px; padding: 6px 0; font-size: 13px; align-items: flex-start; }
.pchk-row > div { min-width: 0; }
.pchk-mark {
  flex: 0 0 auto; width: 19px; height: 19px; border-radius: 50%; margin-top: 1px;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 700;
}
.pchk-row.done .pchk-mark { color: var(--ok-fg); background: var(--ok-soft); }
.pchk-row.todo .pchk-mark { color: var(--warn-fg); background: var(--warn-soft); }
.pchk-row.err .pchk-mark { color: var(--err-fg); background: var(--err-soft); }
.pchk-row.done > div > b { color: var(--muted); font-weight: 500; }
.pchk-row.todo > div > b { color: var(--warn-fg); }
.pchk-row.err > div > b { color: var(--err-fg); }
.pchk-how { margin: 2px 0 0; color: var(--text); font-size: 12.5px; }

/* ===========================================================================
   domain host rows (added → pointed → secured)
   ======================================================================== */

.host-row { padding: 12px 0; border-bottom: 1px dashed var(--border); }
.host-row:last-of-type { border-bottom: none; }
.host-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.host-name { font-family: var(--mono); font-size: 13px; font-weight: 600; word-break: break-all; }
.host-actions { margin-inline-start: auto; display: flex; gap: 6px; flex-wrap: wrap; }
.host-row > .hint, .host-row > .ws-warn { margin-top: 6px; }

/* ---- a website's Domain & SSL page (pages/site.js renderSiteDomains) ----------------
   The 6amMart page's pieces (host-top, the pchk fact rows, ws-warn next steps) plus the
   few a website needs of its own: the notes under the verdict, the www / bare twin that
   rides inside its address's card, the record to make, and the quiet "More". */
.sdom-notes { margin: -6px 0 18px; }
.sdom-notes > :first-child { margin-top: 0; }
.sdom-notes > .hint { margin-top: 8px; }
.sdom-last { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.sdom-last > span { flex: 1 1 260px; min-width: 0; overflow-wrap: anywhere; }
.sdom-card .sdom-host { font-size: 15px; }
.sdom-twin { margin-top: 12px; padding: 10px 12px; border: 1px dashed var(--border); border-radius: var(--r-ctl); }
.sdom-twin > .hint { margin: 6px 0 0; }
.sdom-next .pchk-how { margin-top: 4px; }
.sdom-next .btn-row { margin-top: 10px; }
.sdom-next .tbl-wrap { margin: 8px 0 2px; }
.sdom-next .tbl { font-size: 12.5px; width: auto; min-width: min(100%, 360px); }
.sdom-next .copy-row, .sdom-ip .copy-row { max-width: 420px; }
.sdom-more { margin-top: 14px; }
.sdom-more > summary { cursor: pointer; width: fit-content; }
.sdom-more .btn-row { margin-top: 8px; }
.sdom-add .btn-row { margin-top: 2px; }
.sdom-add .input { flex: 1 1 220px; max-width: 340px; }
details.card > .sdom-add { margin-top: 12px; }
.sdom-help { margin-top: 12px; }

/* ===========================================================================
   disk cleanup checklist + 2FA QR
   ======================================================================== */

.cu-row { align-items: flex-start; padding: 9px 0; border-bottom: 1px dashed var(--border); margin: 0; }
.cu-row:last-of-type { border-bottom: none; }
.cu-row > div { flex: 1; min-width: 0; }
.cu-row input { margin-top: 3px; }
.cu-est { margin-inline-start: auto; color: var(--muted); font-size: 12px; white-space: nowrap; padding-top: 2px; }

.qr-row { display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-start; }
/* THE COLUMN BESIDE THE QR CODE READS LEFT-ALIGNED.
   .login-card centres its hints, which is right for a sentence under a logo and
   wrong for a labelled field: the instruction sat centred over a monospace
   secret and a Copy button, so no edge lined up with anything and the block read
   as three unrelated pieces. Text-align start, and the secret gets the room to
   show all 32 characters instead of ending in an ellipsis. */
.qr-alt { flex: 1 1 260px; min-width: 0; }
.qr-alt .hint { text-align: start; margin: 0 0 8px; }
/* THE SECRET GETS THE WHOLE COLUMN, and the Copy button goes under it.
   Beside a 172px QR there is roughly 300px left, and a 32-character secret needs
   about 250 of it — so sharing the row with a Copy button hid the last characters
   behind the field's edge every time (measured on a 560px card: 255px of text in
   250px of box). A secret whose tail is invisible is worse than one that is
   absent: it gets pasted, silently short, and the code it produces is wrong with
   nothing on screen to explain why. Stacking costs one line of height. */
.qr-alt .copy-row { margin: 0; flex-wrap: wrap; }
.qr-alt .copy-row .input { flex: 1 0 100%; }
.qr-alt .copy-row .btn { flex: 0 0 auto; margin-inline-start: auto; }
.qr-alt details { margin-top: 8px; }
.qr-alt summary.hint { text-align: start; }
/* ---- two-factor setup: two numbered steps, each its own block ----
   The card centres its own text (right for a brand mark and the paragraph under
   it, wrong for instructions with fields beside them), so each step turns that
   off inside itself and reads from the start edge like the form it contains. */
.enrol-steps { display: flex; flex-direction: column; gap: 16px; margin: 4px 0 2px; }
.enrol-step {
  text-align: start;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface-2);
}
.enrol-step-h {
  display: flex; align-items: center; gap: 9px;
  margin: 0 0 4px; font-size: 14px; font-weight: 650; color: var(--text);
}
/* The number, as a real numeral in a disc — the step's own marker, which is what
   let the sentence under it stop starting with "Step N —". */
.enrol-num {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--accent); color: #fff;
  font-size: 12px; font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums;
}
/* !important, and it earns it: `.login-card .hint` sets text-align:center with
   two classes of specificity, so a single-class rule here lost and the sentence
   under each left-aligned title came out centred — the exact mixed alignment
   this restructure existed to remove. The alternative was to repeat the parent
   selector in four places; one flag says the intent once. */
.enrol-step .hint { text-align: start !important; }
.enrol-step-sub { margin: 0 0 12px; }
/* THE QR IS THE STEP, so it is centred and given room rather than squeezed into
   one half of a two-column row. 168px is the size the encoder renders. */
/* A disclosure triangle drawn in text, so it needs no icon and rotates on open.
   11px, not 10: at 10 it rendered as a speck next to 12.5px text and read as a
   stray mark rather than as the control's own marker. */
.enrol-otpauth { margin-top: 8px; }
.enrol-otpauth > summary { cursor: pointer; }
/* NARROW SCREENS: the Copy button moves under the value instead of beside it.
   A 32-character secret and a button cannot share 302px — measured, the secret
   needed 242px and got 206 — and a secret whose last characters are hidden is
   worse than useless: it is pasted, silently short, and the code it produces is
   wrong with nothing to say why. Stacking gives the value the whole width; the
   button loses nothing by being full-width under it. */
.enrol-step form { margin: 0; }
/* an error belongs between the field and the button it explains, not after both */
.enrol-step .login-error { margin: -6px 0 10px; }

/* THE THREE THINGS THAT ARE NOT STEPS, after both of them and quieter than both.
   Centred deliberately, and NOT to match the steps: these are footnotes to the
   whole card, so they belong to the card's own centred column (the brand, the
   title, the paragraph under it) rather than to the left-aligned blocks. The
   steps read left because they contain instructions and fields; a footnote has
   neither. Both alignments in one card is right when each one means something —
   which is the difference from where this started, where the SAME block mixed
   the two for no reason. */
.enrol-tag { text-align: center; margin: 14px 0 0; }
.enrol-note { margin-top: 8px; font-size: 12px; }
.enrol-skip-row { margin-top: 16px; text-align: center; }
.enrol-tag .mono {
  font-size: 13px; letter-spacing: .06em; color: var(--text);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 6px; padding: 1px 6px; margin-inline-start: 4px;
  /* the tag is base32 and reads left-to-right even on an Arabic page */
  direction: ltr; display: inline-block;
}
.qr-box { background: var(--qr-bg); border: 1px solid var(--border); border-radius: 12px; padding: 10px; flex: 0 0 auto; }
.qr-box svg { display: block; width: 150px; height: 150px; }

/* ===========================================================================
   app .env editor
   ======================================================================== */

.env-row {
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  padding: 8px 0; border-bottom: 1px dashed var(--border);
}
.env-row:last-child { border-bottom: none; }
.env-key {
  flex: 0 0 250px; min-width: 0;
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  font-size: 12.5px; word-break: break-all;
}
.env-note { flex-basis: 100%; margin: 0; font-size: 12px; }
.env-row.danger .env-key { color: var(--warn-fg); }
/* a website's environment: NAME | value | remove on ONE line. It used .env-row, whose
   flex-wrap put the full-width inputs on three lines each (the name, the value, then a
   lone ×) — measured on the demo box's Node app, 30 Sep 2026 */
.site-env-row { display: grid; grid-template-columns: minmax(140px, 260px) minmax(0, 1fr) auto; gap: 8px; align-items: center; margin: 6px 0; }
@media (max-width: 620px) {
  .site-env-row { grid-template-columns: minmax(0, 1fr) auto; }
  .site-env-row [data-f="k"] { grid-column: 1 / -1; }
}
/* the key column is a fixed 250px so the controls line up; below that width
   the row stacks instead */
@media (max-width: 620px) {
  .env-key { flex-basis: 100%; }
  .env-ctl { flex-basis: 100%; min-width: 0; }
}

/* schema controls */
.env-ctl { display: flex; align-items: center; gap: 8px; flex: 1 1 260px; min-width: 220px; }
.env-ctl select, .env-ctl input[type="number"] { max-width: 260px; }

/* sticky save bars */
.savebar-sticky {
  position: sticky; bottom: 12px; z-index: 5;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  background: var(--surface);
  box-shadow: var(--shadow-2);
}

/* the notes column: full row width, indented to the control */
.env-notes { flex-basis: 100%; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.env-notes:empty { display: none; }
.env-notes > .env-note { font-size: 12px; line-height: 1.5; }
@media (min-width: 621px) { .env-notes { padding-inline-start: 260px; } }

/* who owns this key — information, not a warning: a quiet accent rule */
.env-note.owned {
  color: var(--muted);
  border-inline-start: 2px solid color-mix(in srgb, var(--accent) 45%, transparent);
  padding-inline-start: 8px;
}
/* what breaks if this is wrong (ENV_DANGER) — the one filled note */
.env-note.crit {
  color: var(--warn-fg); font-weight: 500;
  background: var(--warn-soft);
  border: 1px solid color-mix(in srgb, var(--warn) 24%, transparent);
  border-radius: var(--r-ctl); padding: 7px 10px;
}
/* a legal but discouraged value is selected right now */
.env-note.warnv { color: var(--warn-fg); }
/* the server rejected this field (400 {fields}) */
.env-note.bad { color: var(--err-fg); font-weight: 500; }

/* group heading: vendor .env comment blocks are prose, so wrapping is explicit */
.env-sec { font-size: 13.5px; margin-bottom: 6px; overflow-wrap: anywhere; }
.env-sec-why { margin: 0 0 10px; }

/* "why this group is set this way" — a details that reads as a link */
.env-why > summary {
  cursor: pointer; list-style: none; display: inline-flex; align-items: center; gap: 6px;
  color: var(--accent); font-size: 12.5px; font-weight: 600;
}
.env-why > summary::-webkit-details-marker { display: none; }
.env-why > summary::before { content: "▸"; color: var(--muted); font-weight: 400; }
.env-why[open] > summary::before { content: "▾"; }
.env-why > summary:hover { text-decoration: underline; }
.env-why > p { margin: 6px 0 10px; }

/* groups inside "Show all settings": card chrome dropped, dashed rule instead */
.env-card.grp-sec {
  padding: 16px 0; margin: 0;
  background: none; border: none; box-shadow: none; border-radius: 0;
  border-top: 1px dashed var(--border);
}
.env-card.grp-sec:first-of-type { padding-top: 4px; border-top: none; }

/* the mail card: its note is true regardless of the file's keys */
.env-mail > .ws-warn { margin-top: 0; }

/* before → after list in the save confirmation (spans: nested in a <p>) */
.env-chg-h { display: block; font-size: 12.5px; font-weight: 600; color: var(--text); margin-bottom: 6px; }
.env-chg-h .badge { margin-inline-start: 6px; }
.env-chg {
  display: flex; gap: 10px; flex-wrap: wrap; justify-content: space-between;
  padding: 6px 0; font-size: 12.5px; border-bottom: 1px dashed var(--border);
}
.env-chg:last-of-type { border-bottom: none; }
.env-chg > .mono { color: var(--muted); font-size: 12px; word-break: break-all; }
.env-chg-v { min-width: 0; overflow-wrap: anywhere; text-align: end; }
.env-chg-v i { color: var(--muted); }

/* ===========================================================================
   searchable select (.combo) — country, timezone

   Drawn to match .input exactly, because it IS one: the visible control is a
   text input, and the list is a popover under it. A native <select> would have
   been less code and no search; 247 countries and 400 timezones is what makes
   the search the feature rather than the decoration.
   ======================================================================== */

.combo { position: relative; }
.combo-input { width: 100%; padding-inline-end: 34px; }   /* room for the caret */
/* the caret is drawn, not an icon: it inherits the field's colour and needs no
   second asset or theme swap */
.combo-caret {
  position: absolute; inset-inline-end: 12px; top: 50%;
  width: 8px; height: 8px; margin-top: -6px;
  border-inline-end: 1.7px solid var(--muted);
  border-block-end: 1.7px solid var(--muted);
  transform: rotate(45deg);
  pointer-events: auto; cursor: pointer;
  transition: border-color var(--tick);
}
.combo:focus-within .combo-caret { border-color: var(--accent); }
.combo-list {
  position: absolute; z-index: 60; inset-inline: 0; top: calc(100% + 5px);
  max-height: 264px; overflow-y: auto;
  margin: 0; padding: 5px; list-style: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow-2);
  animation: rise-in .14s ease-out;
  /* momentum scrolling on a trackpad must not scroll the card behind it */
  overscroll-behavior: contain;
}
.combo-list li[role="option"] {
  padding: 7px 10px; border-radius: 8px;
  font-size: 13.5px; color: var(--text);
  cursor: pointer;
  /* long zone names truncate rather than wrap: a two-line row in a list of
     one-line rows reads as a different kind of thing */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* `.on` is the KEYBOARD position and hover is the mouse — both look the same, so
   the two never disagree about which row is about to be chosen. */
.combo-list li[role="option"]:hover,
.combo-list li[role="option"].on { background: var(--accent-soft); color: var(--accent); }
/* the value currently held, marked even when the keyboard is elsewhere */
.combo-list li[aria-selected="true"] { font-weight: 600; }
.combo-list li[aria-selected="true"]::after {
  content: '✓'; float: inline-end; color: var(--accent); font-size: 12px;
}
.combo-empty, .combo-more {
  padding: 7px 10px; font-size: 12.5px; color: var(--muted); cursor: default;
}

/* ===========================================================================
   language dropdown (header, next to the theme toggle)
   ======================================================================== */

.lang-menu { position: relative; flex: 0 0 auto; }
.lang-btn { font-size: 15px; line-height: 1; }
.lang-flag { font-size: 15px; line-height: 1; }
.lang-pop {
  position: absolute; top: calc(100% + 6px); inset-inline-end: 0; z-index: 80;
  min-width: 210px; padding: 6px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 12px; box-shadow: var(--shadow-2);
  animation: rise-in .16s ease-out;
}
.lang-item {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 8px 10px; border: 0; border-radius: 8px;
  background: transparent; color: var(--text); font: inherit; font-size: 13px;
  text-align: start; cursor: pointer;
  transition: background var(--tick);
}
.lang-item:hover { background: var(--accent-soft); }
.lang-item.on { color: var(--accent); font-weight: 600; }
.lang-name { flex: 1; min-width: 0; }
.lang-soon { color: var(--muted); font-weight: 400; font-size: 11px; }
.lang-check { width: 12px; color: var(--accent); font-size: 12px; }

/* ===========================================================================
   NEW first-run wizard — .wz (rail + card over the mesh backdrop)
   The caller rebuilds the wizard DOM against exactly these classes.
   ======================================================================== */

/* full-viewport, content centered (margin:auto on the shell keeps tall
   content scrollable instead of clipped) */
.wz {
  position: fixed; inset: 0; z-index: 150;
  overflow-y: auto;
  display: flex;
  background:
    radial-gradient(46% 38% at 8% 0%, color-mix(in srgb, var(--accent) var(--mesh-a), transparent), transparent 70%),
    radial-gradient(42% 36% at 100% 100%, color-mix(in srgb, var(--warm) var(--mesh-b), transparent), transparent 70%),
    var(--bg);
  padding: 32px 18px;
}
/* PINNED TO THE TOP, NOT CENTRED.
   `margin: auto` centred the shell vertically, so the whole thing — rail
   included — moved every time a step's card changed height. Measured across the
   four steps: the card sat at y=470, 430, then 200, and the rail's numbers slid
   with it, which made stepping through the wizard feel like the page jumping
   rather than the content advancing. The one screen where that is worst is the
   one every buyer walks through.

   Top-aligned, so step 1's rail and step 4's rail are in the same place and only
   the card's own content changes. `margin-inline: auto` keeps the horizontal
   centring; `min-height` gives short steps enough body that the card does not
   look stranded at the top of an empty page. */
.wz-shell {
  margin-inline: auto;
  width: 100%; max-width: 860px;
  display: grid; grid-template-columns: 220px 1fr;
  gap: 28px; align-items: start;
}
/* Vertically centred only while everything fits, and never at the cost of the
   rail moving: the shell keeps its own top edge, and the SPACE above it is what
   flexes. .wz is the flex container, so this is its business, not the shell's. */
.wz { align-items: flex-start; }
@media (min-height: 780px) {
  /* On a tall window the wizard sits a little below the top rather than clamped
     to it — enough to look placed, not enough to move between steps. `max()` so
     this can only ever ADD to the 62px the pre-auth bar needs: the earlier
     version replaced that floor on wide screens, which is how the bar came to
     sit over the card on a 1280x720 laptop. */
  .wz { padding-top: max(62px, 7vh); }
}

/* vertical step rail */
.wz-rail { display: flex; flex-direction: column; gap: 0; padding-top: 8px; }
.wz-step {
  position: relative;
  display: flex; align-items: center; gap: 11px;
  min-height: 40px; padding: 4px 0;
  color: var(--muted); font-size: 13px; font-weight: 550;
}
/* connecting hairline between dots */
.wz-step:not(:last-child)::after {
  content: ""; position: absolute; top: 34px; bottom: -6px;
  inset-inline-start: 12px; width: 2px; border-radius: 99px;
  background: var(--border);
}
.wz-step.done:not(:last-child)::after { background: color-mix(in srgb, var(--accent) 45%, var(--border)); }
.wz-step .dot {
  flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700;
  background: var(--surface); border: 1px solid var(--border-strong); color: var(--muted);
  transition: background var(--tick), border-color var(--tick), color var(--tick);
}
/* done: accent-filled with a ✓ (CSS supplies the glyph only if the dot is empty) */
.wz-step.done { color: var(--text); }
.wz-step.done .dot { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
.wz-step.done .dot:empty::before { content: "✓"; }
/* current: accent ring pulse */
.wz-step.cur { color: var(--text); font-weight: 600; }
.wz-step.cur .dot {
  border-color: var(--accent); color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
  animation: ring-pulse 2.2s ease-out infinite;
}

/* the wizard card */
.wz-card {
  background: var(--surface);
  border-radius: var(--r-modal);
  box-shadow: var(--shadow-2);
  padding: 32px;
  min-width: 0;
}
.wz-title { font-size: 22px; font-weight: 700; letter-spacing: -.01em; margin: 0 0 4px; }
.wz-sub { font-size: 14px; color: var(--muted); margin: 0 0 20px; }
.wz-actions { display: flex; align-items: center; gap: 10px; margin-top: 24px; }

/* clickable suggestion chips */
.wz-suggest { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0; }
.wz-suggest > * {
  display: inline-flex; align-items: center; gap: 6px;
  font: 550 12.5px/1.4 var(--font); color: var(--text);
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: 999px; padding: 6px 13px; cursor: pointer;
  transition: border-color var(--tick), background var(--tick), color var(--tick), box-shadow var(--tick);
}
/* A LABEL INSIDE THE ROW IS NOT A CHIP. `.wz-suggest > *` styles every child as a
   pill, which is right for the suggestions and wrong for the "Or use:" caption
   that introduces them — it arrived looking like a button that does nothing when
   clicked. This is the one child that is text, so it opts out and reads as the
   caption it is. */
.wz-suggest > .note {
  background: none; border: none; border-radius: 0; padding: 0;
  cursor: default; color: var(--muted); font-weight: 500;
}
.wz-suggest > .note:hover { border-color: transparent; color: var(--muted); }
.wz-suggest > *:hover { border-color: var(--accent); color: var(--accent); }
.wz-suggest > *:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.wz-suggest > .sel {
  background: var(--accent-soft); border-color: var(--accent); color: var(--accent); font-weight: 600;
}

/* console-look progress log */
.wz-progress {
  font: 12.5px/1.6 var(--mono);
  color: var(--console-text);
  background: var(--console-bg);
  border: 1px solid var(--console-line);
  border-radius: 12px;
  padding: 12px 14px; margin: 12px 0 0;
  max-height: 220px; overflow: auto;
  white-space: pre-wrap; word-break: break-word;
}

/* success screen: CSS draws the ✓ circle; DOM supplies title + info card */
.wz-success { text-align: center; padding: 8px 0 0; }
.wz-success::before {
  content: "✓";
  display: flex; align-items: center; justify-content: center;
  width: 56px; height: 56px; margin: 0 auto 16px;
  border-radius: 50%;
  background: var(--accent-soft); color: var(--accent);
  font-size: 26px; font-weight: 700;
}
.wz-success .card { text-align: start; margin-top: 18px; box-shadow: none; border: 1px solid var(--border); }

/* EITHER/OR DECISIONS AS CARDS, not as two bare radio buttons.
   UX-PLAN §1 asks for these ("Choice cards for either/or decisions") and until
   now every such choice was a `label.check` pair — a radio and a sentence, which
   reads as a setting rather than as a fork in the road. The first real use is the
   Cloudflare fork in the create-project wizard, where the two paths differ in
   what the OWNER has to do afterwards, so each card has to carry its own
   consequence rather than just a name.
   The whole card is the label, so the click target is the card and not the 14px
   dot; `:has()` styles the selected one from the input's own state, so there is no
   JS keeping a class in sync with a checked property. */
.choice-row {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 10px; margin: 12px 0 0;
}
.choice-card {
  display: grid; gap: 4px; cursor: pointer;
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
  padding: 14px 14px 14px 40px; position: relative;
  transition: border-color .12s ease, background .12s ease;
}
.choice-card:hover { border-color: var(--accent); }
.choice-card > input[type="radio"] {
  position: absolute; left: 14px; top: 15px; width: auto; min-height: 0; margin: 0;
}
.choice-card:has(> input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.choice-card:has(> input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
/* A choice card that IS the button, rather than a label wrapping a radio.
   The radio-bearing card reserves 40px on the inline-start for the control; a
   button has no radio, so it needs that space back or the text sits in a gutter
   of nothing. Same card, same hover, no phantom indent. */
.choice-card.as-btn {
  padding-inline-start: 14px; text-align: start; font: inherit; color: inherit;
}
.choice-card.as-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.choice-title { font-size: 14px; font-weight: 650; color: var(--text); }
.choice-why { font-size: 13px; color: var(--muted); line-height: 1.45; }

/* two-column responsive field grid */
.wz-field-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
/* A field that belongs in a row but has no partner. The language select is one
   short control; full width it read as the most important thing on the form,
   above the email it is not. Half, and it lines up with the column above it. */
.wz-field-half { max-width: calc(50% - 7px); }
@media (max-width: 560px) { .wz-field-half { max-width: none; } }

/* rail furniture around the steps: brand on top, exit link pinned below */
.wz-brand {
  display: flex; align-items: center; gap: 8px;
  font-size: 15px; font-weight: 700; letter-spacing: -.01em;
  margin-bottom: 18px;
}
.wz-brand .brand-icon { width: 26px; height: 26px; }
/* the rail head sets its own 15px; .brand-name is sized for the 36px
   sidebar mark and would override it */
.wz-brand .brand-name { font-size: inherit; }
.wz-rail-steps { display: flex; flex-direction: column; }
.wz-rail-steps .wz-step { background: none; border: 0; cursor: pointer; font-family: var(--font); text-align: start; width: 100%; }
.wz-rail-steps .wz-step:focus-visible { outline: none; }
.wz-rail-steps .wz-step:focus-visible .dot { box-shadow: 0 0 0 3px var(--accent-soft); }
.wz-rail-foot { margin-top: 22px; }
/* The update notice sits INSIDE the card, above the step, because the rail
   foot is display:none at 860px and under — and a phone is where an owner
   reads an alert. It is the only thing in the wizard that is not about setup,
   so it is quiet until there is something to install. */
.wz-update { display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  margin-block-end: 20px; padding: 10px 12px; border: 1px solid var(--border);
  border-radius: 10px; background: var(--surface-2); }
.wz-update-line { flex: 1 1 220px; min-width: 0; font-size: 13px; color: var(--muted); }
.wz-update .btn { flex: 0 0 auto; }
.wz-rail-foot a { color: var(--muted); font-size: 12.5px; cursor: pointer; text-decoration: none; }
.wz-rail-foot a:hover { color: var(--accent); text-decoration: underline; }

/* a quieter card-in-card section (token forms, deploy forms, optional bits) */
.wz-subcard {
  background: var(--surface-2, var(--surface));
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 18px;
  margin: 14px 0 0;
}
.wz-subcard > h3 { margin: 0 0 4px; font-size: 14.5px; font-weight: 650; }
.wz-subcard > .wz-sub { margin-bottom: 12px; }
/* The walkthrough link between a subcard's explanation and its fields. It sits
   ABOVE the inputs on purpose: a reader who does not know what an Account ID is
   needs the manual before the boxes, not after them. `:empty` collapses the
   whole row — docLink() returns an empty string when the manual is not on this
   server, and without this the gap it leaves reads as a missing element. */
.wz-subcard > .wz-doc { margin: 0 0 12px; font-size: 13px; }
.wz-subcard > .wz-doc:empty { display: none; }

/* chained-work timeline: one row per step, live log console under a row */
.wz-chain { display: flex; flex-direction: column; gap: 2px; margin: 16px 0 0; }
.wz-chain-row { display: flex; align-items: flex-start; gap: 10px; padding: 6px 0; }
.wz-chain-dot {
  flex: 0 0 auto; width: 20px; height: 20px; margin-top: 1px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 700;
  background: var(--surface); border: 1.5px solid var(--border-strong); color: var(--muted);
}
.wz-chain-dot.run {
  border-color: var(--accent);
  border-top-color: transparent;
  animation: spin .8s linear infinite;
}
.wz-chain-dot.ok { background: var(--ok-soft, var(--accent-soft)); border-color: transparent; color: var(--ok-fg, var(--ok)); }
.wz-chain-dot.fail { background: var(--err-soft, transparent); border-color: transparent; color: var(--err-fg, var(--err)); }
.wz-chain-dot.skip { opacity: .5; }
.wz-chain-main { flex: 1; min-width: 0; }
.wz-chain-label { font-size: 13.5px; font-weight: 550; padding-top: 1px; }
.wz-chain-note { font-size: 12.5px; color: var(--err-fg, var(--err)); margin-top: 2px; }
.wz-chain-main > .wz-progress { margin-top: 8px; max-height: 160px; }
@keyframes spin { to { transform: rotate(360deg); } }

/* input + button on one line */
.wz-inline { display: flex; gap: 8px; }
.wz-inline .input { flex: 1; min-width: 0; }

/* "what runs automatically" list on the finish screen */
.wz-auto-list { list-style: none; padding: 0; margin: 0 0 18px; }
.wz-auto-list li {
  position: relative; padding: 7px 0 7px 26px; font-size: 13.5px;
}
.wz-auto-list li::before {
  content: "✓"; position: absolute; left: 0; top: 6px;
  width: 18px; height: 18px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent-soft); color: var(--accent);
  font-size: 11px; font-weight: 700;
}
.wz-auto-list li.off::before { content: "!"; background: var(--warn-soft); color: var(--warn-fg); }
[dir="rtl"] .wz-auto-list li { padding: 7px 26px 7px 0; }
[dir="rtl"] .wz-auto-list li::before { left: auto; right: 0; }

/* ===========================================================================
   a website's Deploys page (renderSiteDeploys) — the 6amMart Deploys page's
   language: one card that says where the website stands, then the ways in
   ======================================================================== */

/* the empty state: an icon, the kind's own sentence, then the three ways in */
.sdep-hero-head { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 16px; }
.sdep-hero-head h3 { margin: 0 0 4px; font-size: 17px; }
.sdep-hero-head .hint { margin: 0; max-width: 84ch; }
.sdep-hero-ic {
  flex: 0 0 auto; width: 44px; height: 44px; border-radius: 12px;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent-soft); color: var(--accent);
}
.sdep-acts { margin-top: 14px; }

/* the three sources, as cards that are the buttons */
.sdep-sources { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin: 0 0 14px; }
.sdep-src {
  display: flex; align-items: flex-start; gap: 10px; text-align: start;
  padding: 12px 14px; border-radius: 12px; cursor: pointer;
  border: 1px solid var(--border-strong); background: var(--surface); color: var(--text);
  font: 13px/1.45 var(--font);
  transition: border-color var(--tick), background var(--tick), box-shadow var(--tick);
}
.sdep-src > svg { flex: 0 0 auto; color: var(--accent); margin-top: 1px; }
.sdep-src b { display: block; font-size: 13.5px; font-weight: 650; }
.sdep-src span span { display: block; color: var(--muted); font-size: 12px; }
.sdep-src:hover { border-color: var(--accent); }
.sdep-src:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.sdep-src.sel {
  border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent);
  background: color-mix(in srgb, var(--accent-soft) 55%, var(--surface));
}
.sdep-src:disabled { opacity: .55; cursor: not-allowed; }
@media (max-width: 720px) { .sdep-sources { grid-template-columns: 1fr; } }

/* the drop zone; a ZIP or a folder dropped anywhere on the page lands here */
.sdep-drop {
  display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center;
  padding: 26px 18px; border: 1.5px dashed var(--border-strong); border-radius: 14px;
  background: var(--surface-2);
  transition: border-color var(--tick), background var(--tick);
}
.sdep-drop b { font-size: 14px; font-weight: 650; }
.sdep-drop .hint { margin: 0; max-width: 60ch; }
.sdep-drop > .btn { margin-top: 6px; }
.sdep-drop-ic { color: var(--accent); display: inline-flex; }
.sdep-drop.over { border-color: var(--accent); background: color-mix(in srgb, var(--accent-soft) 70%, var(--surface)); }
.sdep-drop.picked { border-style: solid; border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
.sdep-drop.off { opacity: .6; }
.sdep-name { overflow-wrap: anywhere; }
.sdep-bar { width: min(360px, 100%); height: 6px; border-radius: 999px; background: var(--well); overflow: hidden; margin-top: 6px; }
.sdep-bar > i { display: block; width: 0; height: 100%; background: var(--accent); transition: width .2s ease-out; }
.sdep-go { margin-top: 10px; justify-content: center; }
.sdep-repo + .sdep-go { justify-content: flex-start; }
.sdep-info {
  font-size: 12.5px; color: var(--info-fg); background: var(--info-soft);
  border: 1px solid color-mix(in srgb, var(--info) 22%, transparent);
  border-radius: 10px; padding: 9px 12px; margin: 12px 0 0;
}

/* the saved repository, with its one button below */
.sdep-repo {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 12px 14px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface-2);
}
.sdep-repo-ic { color: var(--accent); display: inline-flex; flex: 0 0 auto; }
.sdep-repo-main { flex: 1 1 240px; min-width: 0; }
.sdep-repo-main .mono { font-size: 13px; overflow-wrap: anywhere; }
.sdep-repo-main .hint { margin: 2px 0 0; }

/* "Live now" and the checklist: a verdict card with rows under its head */
.health-banner.sdep-live { display: block; }
/* one row while it fits; on a phone the chip and the button go under it. A long
   title still wraps inside itself instead of leaving its light alone above it: for
   line breaking its size is capped at the row less the light (14px) and the gap, so
   it always fits beside the light. Measured at 390px (review of 30 Sep 2026): "Live
   now" with a "last deploy failed" chip and Open site ran to 464px in German. */
.sdep-head { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; }
.sdep-head > * { flex: 0 0 auto; }
.sdep-head > .health-verdict, .sdep-head > h3 { flex: 0 1 auto; min-width: 0; max-width: calc(100% - 24px); }
.sdep-head > .spacer { flex: 1 1 auto; }
.sdep-head h3 { margin: 0; }
/* chips that carry a sentence — a delivery's result, the build command — wrap inside
   the card instead of widening the page (411-702px measured at 390) */
.sdep-head > .chip, .sdep-kvs .chip, .sdep-chips .chip { white-space: normal; max-width: 100%; }
.sdep-live > .hint, .sdep-auto > .hint { margin: 6px 0 0; }
/* label beside value, read left to right: in a full-width card the .kv default
   (value pushed to the far edge) leaves the value a long way from its label */
.sdep-kvs { max-width: 760px; margin-top: 10px; }
.sdep-kvs .kv { justify-content: flex-start; gap: 16px; }
.sdep-kvs .kv > .k { flex: 0 0 120px; }
.sdep-kvs .kv > span:last-child { min-width: 0; overflow-wrap: anywhere; }
.sdep-kvs .kv .hint { margin: 1px 0 0; font-size: 12px; }
.sdep-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
/* a collapsed card's title and its one-line hint: side by side, the hint under
   the title on a phone (beside it, a two-word title wraps into a column). EVERY such
   card, not only a website's Deploys ones: "Add another address" on a website's Domain &
   HTTPS stood as a column of single words beside its hint (demo box, 3 Oct 2026), and
   6amMart's Deploys, Database, Health and Cron cards carry the same pair. The summary
   becomes a block, so the marker stays inline before the title and a long title wraps
   as text — a wrapping flex row would drop it under a lone marker instead. */
@media (max-width: 560px) {
  details.card > summary:has(> .hint) { display: block; }
  details.card > summary:has(> .hint)::before { display: inline-block; margin-inline-end: 8px; }
  details.card > summary > .hint { display: block; padding-inline-start: 18px; margin-top: 2px; }
}
.sdep-flash { animation: sdep-flash 1.1s ease-out 1; }
@keyframes sdep-flash {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 45%, transparent); }
  100% { box-shadow: 0 0 0 14px transparent; }
}

/* the Node.js first-run checklist: files read, environment, domain */
.sdep-steps { list-style: none; margin: 12px 0 0; padding: 0; }
.sdep-step { display: flex; align-items: flex-start; gap: 12px; padding: 12px 0; border-top: 1px dashed var(--border); }
.sdep-step:first-child { border-top: 0; padding-top: 4px; }
.sdep-step-dot {
  flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700; color: var(--muted);
  background: var(--surface); border: 1.5px solid var(--border-strong);
}
.sdep-step.done .sdep-step-dot { background: var(--ok-soft); border-color: transparent; color: var(--ok-fg); }
.sdep-step.warn .sdep-step-dot { background: var(--warn-soft); border-color: transparent; color: var(--warn-fg); }
.sdep-step.bad .sdep-step-dot { background: var(--err-soft); border-color: transparent; color: var(--err-fg); }
.sdep-step.cur .sdep-step-dot { border-color: var(--accent); color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.sdep-step.run .sdep-step-dot { border-color: var(--accent); border-top-color: transparent; animation: spin .8s linear infinite; }
.sdep-step-main { flex: 1 1 auto; min-width: 0; }
.sdep-step-main b { display: block; font-size: 13.5px; font-weight: 650; }
.sdep-step-main .hint { margin: 2px 0 0; overflow-wrap: anywhere; }
.sdep-step > .btn, .sdep-step > .btn-row { flex: 0 0 auto; align-self: center; }
@media (max-width: 560px) {
  .sdep-step { flex-wrap: wrap; }
  .sdep-step-main { flex-basis: calc(100% - 38px); }
  .sdep-step > .btn, .sdep-step > .btn-row { margin-inline-start: 38px; }
}
@media (prefers-reduced-motion: reduce) { .sdep-step.run .sdep-step-dot, .sdep-flash { animation: none; } }

/* automatic deploys inside the status card, as on 6amMart's Deploys page */
.sdep-autobox .kv, .sdep-auto .kv { justify-content: flex-start; gap: 16px; }
.sdep-autobox .kv > .k, .sdep-auto .kv > .k { flex: 0 0 120px; }
.sdep-autobox > .hint, .sdep-auto > .hint { margin: 4px 0 0; }
.sdep-autobox .sdep-acts, .sdep-auto .sdep-acts { margin-top: 10px; }
.sdep-repo-url { overflow-wrap: anywhere; word-break: break-all; }
/* the git tab before a repository is connected: one button that opens the window */
.sdep-gitcta { border-style: solid; }
/* the Connect git and automatic-deploys windows */
.sg-summary { margin: 4px 0 12px; }
.sg-summary .kv > .k { flex: 0 0 110px; }
.sg-sub { margin: 18px 0 2px; font-size: 14px; }
#sgBody .verdict, #saBody .verdict { margin-top: 4px; }
#sgAuto .sdep-steps, #saBody .sdep-steps { margin-top: 6px; }
#sgAuto .sdep-step-main .field, #saBody .sdep-step-main .field { margin-top: 8px; }
.modal .modal-actions .spacer { flex: 1 1 auto; }
@media (max-width: 560px) { .modal .modal-actions { flex-wrap: wrap; } }

/* automatic deploys: the forge's steps under a quiet disclosure */
.sdep-how { margin-top: 12px; }
.sdep-how > summary { cursor: pointer; font-size: 13px; font-weight: 600; color: var(--accent); }
.sdep-how[open] > summary { margin-bottom: 10px; }
.sdep-forge { margin: 4px 0 10px; }
.sdep-how-steps { margin: 0 0 10px; padding-inline-start: 20px; font-size: 13px; line-height: 1.6; }
.sdep-how-steps li + li { margin-top: 3px; }

/* history: the live row reads as the one that is live */
.sdep-hist tr.sdep-row-live td { background: color-mix(in srgb, var(--ok-soft) 45%, transparent); }
.sdep-hist td .hint { margin: 1px 0 0; font-size: 12px; }
.sdep-hist td.sdep-nowrap, .sdep-jobs td.sdep-nowrap { white-space: nowrap; }
/* On a phone the version column (a commit subject, an AI request) got ~95 px and
   wrapped a word a line, while Size and "Make live" sat off-screen behind a sideways
   scroll. Here each version is a small block — when and state, the version, size
   and its button — decided for this table alone (the note under .tbl: stacking is a
   per-table call, not a breakpoint for every table). */
@media (max-width: 560px) {
  .sdep-hist, .sdep-hist tbody { display: block; }
  .sdep-hist thead { display: none; }
  .sdep-hist tr {
    display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 10px; row-gap: 4px;
    grid-template-areas: "when st" "ver ver" "size act";
    padding: 10px 12px; border-bottom: 1px solid var(--border);
  }
  .sdep-hist tr:last-child { border-bottom: 0; }
  .sdep-hist td { padding: 0; border: 0; background: none; }
  .sdep-hist tr.sdep-row-live { background: color-mix(in srgb, var(--ok-soft) 45%, transparent); }
  .sdep-hist tr.sdep-row-live td, .sdep-hist tr:hover td { background: none; }
  .sdep-hist td.sdep-when { grid-area: when; color: var(--muted); font-size: 12px; align-self: center; }
  .sdep-hist td.sdep-st { grid-area: st; justify-self: end; }
  .sdep-hist td.sdep-ver { grid-area: ver; }
  .sdep-hist td.sdep-size { grid-area: size; align-self: center; }
  .sdep-hist td.sdep-act { grid-area: act; }
  .sdep-hist td.sdep-act:empty, .sdep-hist td.sdep-size:empty { display: none; }
  /* the jobs list the same way: the job's name, then its state, when, and its log */
  .sdep-jobs, .sdep-jobs tbody { display: block; }
  .sdep-jobs thead { display: none; }
  .sdep-jobs tr {
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto; column-gap: 10px; row-gap: 4px;
    grid-template-areas: "job job job" "st when act"; align-items: center;
    padding: 10px 12px; border-bottom: 1px solid var(--border);
  }
  .sdep-jobs tr:last-child { border-bottom: 0; }
  .sdep-jobs td { padding: 0; border: 0; background: none; }
  .sdep-jobs tr:hover td { background: none; }
  .sdep-jobs td.sdep-job { grid-area: job; overflow-wrap: anywhere; }
  .sdep-jobs td.sdep-st { grid-area: st; }
  .sdep-jobs td.sdep-when { grid-area: when; color: var(--muted); font-size: 12px; }
  .sdep-jobs td.sdep-act { grid-area: act; }
}

/* ===========================================================================
   v2 IA — tab bar (merged pages), Home, Logs
   ======================================================================== */

/* tab bar on merged pages (Server, Advanced, Settings, Logs) */
.tabbar {
  display: flex; gap: 4px; flex-wrap: wrap;
  border-bottom: 1px solid var(--border);
  margin-bottom: 18px;
}
.tabbar .tab {
  background: none; border: 0; cursor: pointer;
  font: 550 13px/1 var(--font); color: var(--muted);
  padding: 9px 13px; border-radius: var(--r-ctl) var(--r-ctl) 0 0;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  transition: color var(--tick), border-color var(--tick), background var(--tick);
}
.tabbar .tab:hover { color: var(--text); background: var(--surface-2); }
.tabbar .tab.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.tabbar .tab:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-soft); }

/* Home — attention feed, vitals, project cards */
.home-allclear { margin-bottom: 16px; }
/* the one line Home keeps: how many things there are, and the way into the bell */
.home-attention { margin-bottom: 16px; }
.home-attention > h3 { margin: 0 0 10px; font-size: 14.5px; }
.att-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; }
.att-row + .att-row { border-top: 1px solid var(--border); }
.att-main { flex: 1; min-width: 0; }
/* long class names and paths break anywhere: the vendor project's Overview quoted
   Modules\RideShare\Interface\…\DriverLevelServiceInterface and ran to 525px on a phone */
.att-title { font-size: 13.5px; font-weight: 550; overflow-wrap: anywhere; }
.att-detail { font-size: 12.5px; color: var(--muted); margin-top: 1px; overflow-wrap: anywhere; }
/* a stack trace's file:line on the Logs page (vendor/laravel/framework/src/…) */
.log-where { overflow-wrap: anywhere; }
/* ON A PHONE THE SAME EIGHT ROWS ARE TWICE AS TALL, and that is a layout
   problem rather than a content one. Measured on the amd64 box, 390px viewport,
   6 Sep: the attention card alone was 1293 px and one row 173 px, because each
   row's explanation reflows to six or seven lines at that width — the whole page
   came to 4496 CSS px.
   NOTHING IS HIDDEN OR TRUNCATED HERE. Every row, its title, its full sentence
   and its button stay exactly as they are; only the spacing and the leading
   tighten, and the button moves under the text instead of competing with it for
   the same line. Clamping the sentence was the obvious move and is the wrong
   one: the feed already prints only firstSentence(detail), so cutting that
   further would leave a row that says a project needs attention and not why —
   and the 5 Sep round is three reverts' worth of evidence that removing needs
   the same proof as adding. */
@media (max-width: 560px) {
  .att-row {
    align-items: flex-start;
    flex-wrap: wrap;
    padding: 9px 0;
    gap: 8px;
  }
  .att-row .dot-svc { margin-top: 5px; }
  .att-detail { font-size: 12px; line-height: 1.45; }
  /* THE OBVIOUS FIX MADE IT WORSE, so it is not here. `.att-main { flex: 1 0
     100% }` does force the button onto its own line — and measured on the box it
     took the eight rows from 1175px to 1402px, because the button's new line
     costs more than the wider text saves. Reverted rather than kept: the row is
     already at its measured best with the button inline.
     The remaining height is the sentence itself, not the layout — 218 characters
     at ~27 per line. That belongs to firstSentence()'s 220-character cap in
     home.js, and cutting it there would leave a row that says a project needs
     attention without saying why, which is the thing this round exists to stop.
     Left alone deliberately; see docs/measurements/2026-09-06. */
  .att-row > .btn { margin-inline-start: 17px; }
}
.dot-svc {
  flex: 0 0 auto; width: 9px; height: 9px; border-radius: 50%;
  background: var(--border-strong);
}
.dot-svc.ok { background: var(--ok); }
.dot-svc.err { background: var(--err); }
.dot-svc.warn { background: var(--warn); }
.home-vitals-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.home-vitals-head h3 { margin: 0 0 10px; font-size: 14.5px; }
.home-vitals { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; }
/* The chart itself: it stretches to its cell (preserveAspectRatio="none" in the
   SVG), and takes its colour from `color` so it themes for free in dark mode.
   Rendered at --muted it was almost invisible — a line you have to hunt for is
   not information — so it uses the same accent the meters do at a weight that
   reads at 26px tall. `overflow: hidden` matters too: with `visible`, a stroke at
   0% or 100% spilled half its width out of the cell and the CPU line appeared to
   cross into the memory column. */
.spark {
  display: block; width: 100%; height: 26px; overflow: hidden;
  color: var(--accent); opacity: .85;
}
/* The 90% danger line. Its own colour rather than `currentColor`, because the
   whole point is that it is NOT the data — and faint, because it is a reference
   the eye should find only when the data reaches it. */
.spark-danger { stroke: var(--err); opacity: .45; }

/* ---- Monitoring card ---- */
/* Its own card, with the range switcher on the header row. `auto-fit` with a
   floor rather than three fixed columns: on a phone the three charts stack, and
   a 33%-wide chart at 360px is a smudge. */
.mon-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.mon-head h3 { margin: 0; font-size: 14.5px; }
.mon-ranges { display: flex; gap: 6px; flex-shrink: 0; }
.mon-charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-top: 4px; }
.mon-cell { min-width: 0; }
/* Label left, current value right — the value is the answer, the chart is the
   context, so they share a line above it. */
.mon-cell-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 5px; }
.mon-cell-label { font-size: 13px; color: var(--muted); }
/* A boxed plot, so an empty or gappy series still reads as a chart rather than
   as a failure to draw — the same reasoning as .home-trend-cell's floor, which
   this replaces. */
.mon-plot {
  border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface-2, transparent); padding: 4px 6px; overflow: hidden;
}
.mon-plot .spark { height: 44px; }
.mon-svc { margin-top: 14px; }
.mon-svc > summary { cursor: pointer; font-size: 13px; color: var(--muted); }
.mon-svc > summary:hover { color: var(--text); }
.mon-svc-note { margin-top: 8px; }
/* A tab's one-line lead, above its cards. Muted and unboxed: it is orientation,
   not content, and a card around it would make it look like a section of its
   own. */
.tab-lead { margin: 0 0 14px; color: var(--muted); font-size: 13px; max-width: 88ch; }
.home-infra { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 14px; }
.home-projects-head { display: flex; align-items: center; justify-content: space-between; margin: 20px 0 12px; }
.home-projects-head h3 { margin: 0; font-size: 14.5px; }
.home-projects { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 14px; }
.proj-card-name { font-size: 15px; font-weight: 650; color: var(--text); text-decoration: none; }
.proj-card-name:hover { color: var(--accent); }
.proj-card-dots { display: flex; gap: 4px; margin-inline-start: auto; }
.proj-card-domain { font-family: var(--mono); font-size: 12.5px; color: var(--muted); margin: 2px 0 10px; }
.proj-card-meta { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.proj-card-actions { margin: 0; }
.home-foot { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin-top: 20px; }
.home-foot h3 { margin: 0 0 8px; font-size: 14.5px; }
/* the Backups card of a server never backed up: the band, then its one button (home.js backupsCard) */
.home-foot .verdict { margin-bottom: 12px; }

/* verdict bands — one tinted sentence: ok / warn / err */
.verdict {
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px 16px;
  font-size: 13.5px;
}
/* TWO VERDICTS IN A ROW NEED AIR BETWEEN THEM. `.verdict` carried no margin at
   all, which was invisible while only ever one was shown. The Cloudflare step now
   stacks two — "this domain is on Cloudflare" and then why the token cannot use
   it — and they met border-to-border, reading as one two-tone box.
   An adjacent-sibling rule rather than a margin on every verdict: a single one
   still sits exactly where its container puts it, so nothing else moves. */
.verdict + .verdict { margin-top: 10px; }
.verdict.ok { background: var(--ok-soft); border-color: color-mix(in srgb, var(--ok) 30%, var(--border)); color: var(--ok-fg); }
.verdict.warn { background: var(--warn-soft); border-color: color-mix(in srgb, var(--warn) 30%, var(--border)); color: var(--warn-fg); }
.verdict.err { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 30%, var(--border)); color: var(--err-fg); }
.verdict .hint { color: inherit; opacity: .85; }

/* Backups hero */
.backup-hero { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; margin-bottom: 16px; }

/* project Overview status hero */
.ov-hero-urls { display: flex; flex-direction: column; gap: 6px; }
.ov-hero-url { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.ov-hero-url .k { color: var(--muted); font-size: 12.5px; font-weight: 550; flex: 0 0 44px; }
.ov-hero-url a.mono { font-size: 13.5px; color: var(--text); text-decoration: none; }
.ov-hero-url a.mono:hover { color: var(--accent); }
.ov-hero-chips { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.ov-hero-dots { display: inline-flex; gap: 4px; padding: 0 4px; }

/* Logs page */
.logs-pane { max-height: 62vh; margin-top: 8px; }
.logs-err { padding: 12px 16px; margin-bottom: 10px; }
.logs-err-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.logs-err-msg { font-family: var(--mono); font-size: 12.5px; word-break: break-word; }

/* ===========================================================================
   motion
   ======================================================================== */

@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes rise-in { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@keyframes toast-in { from { opacity: 0; transform: translateX(12px); } to { opacity: 1; transform: none; } }
[dir="rtl"] .toast { animation-name: toast-in-rtl; }
@keyframes toast-in-rtl { from { opacity: 0; transform: translateX(-12px); } to { opacity: 1; transform: none; } }
@keyframes drawer-up { from { transform: translateY(24px); opacity: .4; } to { transform: none; opacity: 1; } }
@keyframes ring-pulse {
  0%   { box-shadow: 0 0 0 3px var(--accent-soft); }
  55%  { box-shadow: 0 0 0 7px color-mix(in srgb, var(--accent) 10%, transparent); }
  100% { box-shadow: 0 0 0 3px var(--accent-soft); }
}

/* ===========================================================================
   responsive
   ======================================================================== */

@media (max-width: 860px) {
  .shell { flex-direction: column; }
  /* TWO ROWS, not one. As a single row the nav was `flex: 1` on whatever the
     brand, the version and the Log out button left over — 126px of a 390px
     phone, against 362px of icons. Three of nine project pages were reachable;
     six were off-screen behind a drag nothing hinted at. And when the version
     chip appeared — 'update ready', i.e. exactly the state the product steers
     every buyer into after a release — the nav collapsed to ZERO width and the
     panel had no navigation at all. Giving the nav its own full-width row means
     nothing else can take its space, whatever the chip says or how long the
     word is in the reader's language. */
  .sidebar {
    width: 100%; flex: none; height: auto; position: static;
    flex-direction: row; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px 12px;
    border-inline-end: none; border-block-end: 1px solid var(--border);
  }
  /* MARK ONLY on a phone. The strip is one row and carries the whole nav plus
     Log out; the product NAME is what a small screen can lose without losing a
     function. The version and the update chip STAY — a phone is where a shop
     owner actually reads this panel. The name stays in the DOM for a screen
     reader, hidden visually only. */
  .brand { padding: 0 6px; gap: 0; }
  .brand::after { display: none; }
  .brand-icon { width: 26px; height: 26px; }
  .brand .brand-name {
    position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }
  /* one row on the strip, never wrapping: the sidebar is horizontal here and a
     wrapped chip would push the nav down a line */
  .brand-sub { display: flex; align-items: center; flex-wrap: nowrap; padding: 0 4px; }
  .nav {
    flex-direction: row; overflow-x: auto; align-items: center; gap: 6px;
    /* its own row, the full width of the strip */
    order: 2; flex: 1 0 100%; min-width: 0; padding: 4px 0 2px;
    scroll-padding-inline: 24px;
    /* the drag is invisible on a touch overlay scrollbar, so the edge of the
       strip fades when there is more to reach — .nav.scrolls is set by
       core.js buildNav() only when the content really does overflow */
    -webkit-overflow-scrolling: touch;
  }
  .nav.scrolls { mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%); }
  .nav.scrolls:dir(rtl) { mask-image: linear-gradient(to left, #000 calc(100% - 28px), transparent 100%); }
  /* the start edge fades too once the strip has scrolled away from it (core.js
     navEdges): on Settings, Home had gone past the start edge with no sign */
  .nav.scrolls.more-start { mask-image: linear-gradient(to right, transparent 0, #000 28px); }
  .nav.scrolls.more-start:dir(rtl) { mask-image: linear-gradient(to left, transparent 0, #000 28px); }
  .nav.scrolls.more-start.more-end { mask-image: linear-gradient(to right, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%); }
  .nav.scrolls.more-start.more-end:dir(rtl) { mask-image: linear-gradient(to left, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%); }
  .brand, .brand-sub, .sidebar-foot { order: 1; }
  .sidebar-foot { margin-inline-start: auto; }
  /* EVERY ENTRY CARRIES ITS WORD (round 157). The strip was drawings only — `.nav a span
     { display: none }` — and a phone has no hover, so an entry's `title` never shows: the
     owner found a page by pressing drawings until the page's own title said the right
     word. Measured at 390px, 10 Oct 2026: 0 of 11 entries with a word on the panel's menu,
     0 of 12 on a project's, 0 of 16 on a WordPress website's. (It was sized so "the
     panel's ten pages fit a 390px phone"; there are eleven, and a website has up to
     seventeen — 578px of strip in 366 — so the last ones were past the edge either way.)
     The word sits under the drawing, small. The strip is longer for it and scrolls, as it
     already did: the edge fades above say there is more, and core.js navReveal() brings
     the page you are on into view. 44px wide at least: a short word is still a finger's
     width. `flex: 0 0 auto` because that floor replaces `min-width: auto`: without it the
     strip, being over-full by design, shrank the one entry that is its own flex item — the
     back link — to 44px, and "All projects" hung out of both sides of it, cut by the
     screen's edge on one and against the next entry on the other (seen in a picture of a
     website's page, 10 Oct 2026; no rule of the audit names it). */
  .nav a { flex: 0 0 auto; flex-direction: column; justify-content: center; gap: 3px; padding: 5px 8px 6px; min-width: 44px; min-height: 46px; white-space: nowrap; }
  .nav a span { font-size: 10.5px; line-height: 1.1; font-weight: 600; }
  .nav a.active::before {                  /* underline marker when horizontal */
    top: auto; inset-inline: 8px; bottom: 0;
    width: auto; height: 3px; background: var(--accent);
  }
  .nav-group { flex-direction: row; align-items: center; gap: 2px; }
  .nav-group + .nav-group { margin-block-start: 0; margin-inline-start: 10px; padding-inline-start: 10px; border-inline-start: 1px solid var(--border); }
  .nav-group-title { display: none; }
  .nav-group-head { margin: 0; padding: 0; }
  #nav a.nav-back { margin-bottom: 0; }
  .sidebar-foot { padding: 0; }
  .main { padding: 0 16px 120px; }
  .page-head { margin: 0 -16px 20px; padding: 9px 16px; }
  .page-head-text { flex-basis: 100%; }   /* phones: title on its row, tools below */
  .card { padding: 16px; }
  .drawer { height: 55vh; }
  .setup-wrap { padding: 16px 12px 44px; }
  .wiz-name { display: none; }          /* dots only on small screens */
  .modal.fm-editor-modal { width: 100%; }
  .fm-editor-modal textarea { height: 50vh; }
}

/* 190px, not 130: at 130 every project page on a phone read "Optimised Sh▾" (demo box, 3 Oct
   2026) with ~100px of the tools row unused. 190 + the three 34px buttons and their gaps is
   316px, inside a 360px phone's 328. (The cap is the switcher's own: it was a <select>, it is
   the pill of .proj-menu now.) */
@media (max-width: 720px) { .head-tools .proj-menu { max-width: 190px; } }

/* new wizard: rail becomes a horizontal dots row above the card */
@media (max-width: 760px) {
  .wz { padding: 18px 12px; }
  .wz-shell { grid-template-columns: 1fr; gap: 18px; }
  .wz-rail { flex-direction: row; align-items: center; justify-content: center; gap: 10px; padding: 0; }
  .wz-brand, .wz-rail-foot { display: none; }   /* dots row only above the card */
  .wz-rail-steps { flex-direction: row; gap: 10px; }
  .wz-step { min-height: 0; padding: 0; }
  .wz-step > :not(.dot) { display: none; }   /* dots only */
  .wz-step:not(:last-child)::after { display: none; }
  .wz-card { padding: 22px 18px; }
  .wz-title { font-size: 19px; }
}

/* ===========================================================================
   RTL (ar) — flex/grid + logical properties mirror the layout from dir="rtl"
   alone; what is left is forcing LTR islands for machine text, plus the two
   physical effects logical properties cannot express.
   ======================================================================== */

/* logs, cron expressions, paths and domains stay LTR inside an RTL page */
[dir="rtl"] .mono,
[dir="rtl"] .log-pre,
[dir="rtl"] .drawer-log,
[dir="rtl"] .wz-progress,
[dir="rtl"] .aic-diff,
[dir="rtl"] code { direction: ltr; text-align: left; unicode-bidi: isolate; }
[dir="rtl"] .copy-row .input { direction: ltr; text-align: left; }
[dir="rtl"] .fm-crumbs { direction: ltr; text-align: left; }
[dir="rtl"] .fm-strip-cur { text-align: left; }
[dir="rtl"] .fm-editor-modal textarea { direction: ltr; text-align: left; }

/* ===========================================================================
   reduced motion: no movement, no pulses, no shimmer
   ======================================================================== */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  .btn.primary:hover, .btn.danger:hover { transform: none; }
  .skel-bar { animation: none; }
  .wiz-node.cur .wiz-dot, .wz-step.cur .dot { animation: none; }
}

/* ================= running-job pill + busy strips =========================
   One job runs at a time server-wide; the pill sits in the page header on
   every screen while one does, and .busy-strip is what guardBusy() paints
   above a form it has switched off. */
.job-pill {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 12px; border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
  background: var(--warn-soft); color: var(--warn-fg);
  font: inherit; font-size: 12.5px; cursor: pointer; max-width: 260px;
}
.job-pill:hover { filter: brightness(1.06); }
.job-pill-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.job-pill-more { opacity: .7; }
.job-pill-dot {
  width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto;
  background: var(--warn);
  animation: jobPulse 1.2s ease-in-out infinite;
}
@keyframes jobPulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
.busy-strip {
  display: flex; align-items: center; gap: 9px;
  margin: 0 0 12px; padding: 9px 12px; border-radius: 10px;
  border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
  background: var(--warn-soft); color: var(--warn-fg);
  font-size: 13px;
}
.busy-strip .btn { margin-left: auto; flex: 0 0 auto; }
@media (max-width: 720px) { .job-pill { max-width: 150px; } }
@media (prefers-reduced-motion: reduce) { .job-pill-dot { animation: none; } }
.job-progress h3 { display: flex; align-items: center; gap: 9px; }

/* SixPanel Pro, said right after the version when this server has it (index.html
   #proBadge in .brand-sub, core.js drawProBadge). The badge is the house's own
   (.badge.acc); the link around it keeps a button's height, so it is as easy to press as
   one, and it never gives way in its row. */
.pro-head { display: inline-flex; align-items: center; flex: none; min-height: 34px; border-radius: 7px; text-decoration: none; }
.pro-head:hover { text-decoration: none; }
.pro-head:hover .badge { background: color-mix(in srgb, var(--accent) 18%, var(--accent-soft)); }
.pro-head:focus-visible { outline: none; }
.pro-head:focus-visible .badge { box-shadow: 0 0 0 3px var(--accent-soft), 0 0 0 1px var(--accent); }
@media print { .pro-head { display: none !important; } }

/* ================= the bell: what waits for the owner =======================
   In the page head on every screen (index.html #attnMenu, core.js paintAttention).
   The number is every row the open bell lists. While one of them cannot wait — an
   approval, a check that fails, an open incident — it wears the colours of Tia's own
   badge, so one red number means one thing in this panel; while every row is a warning
   it is the house's amber (.warn — the chip's own pair), and the bell itself stays quiet. */
.attn-menu { position: relative; flex: 0 0 auto; }
.attn-btn { position: relative; }
.attn-btn.has { color: var(--text); }
.attn-count {
  position: absolute; top: -6px; inset-inline-end: -6px;
  min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px;
  background: var(--err); color: var(--on-solid);
  font-size: 11px; font-weight: 600; line-height: 18px; text-align: center;
  box-shadow: 0 0 0 2px var(--bg); pointer-events: none;
}
.attn-count.warn { background: var(--warn-soft); color: var(--warn-fg); box-shadow: 0 0 0 1px color-mix(in srgb, var(--warn) 45%, transparent), 0 0 0 3px var(--bg); }
/* Under the button, from its end edge inwards: the tools sit at the end of the head, so
   that is where the room is. The list scrolls inside the box — a page behind an open
   bell must not scroll with it. */
.attn-pop {
  position: absolute; top: calc(100% + 8px); inset-inline-end: 0; z-index: 80;
  width: min(440px, calc(100vw - 32px)); max-height: min(68vh, 620px);
  overflow-y: auto; overscroll-behavior: contain;
  padding: 14px 16px 12px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 14px; box-shadow: var(--shadow-2);
  animation: rise-in .16s ease-out;
  text-align: start;
  /* …and what a <dialog> brings of its own, taken back: it centres itself between both
     inline edges with an auto margin, sizes to its content and wears the system colours */
  inset-inline-start: auto; margin: 0; height: auto; color: var(--text);
}
/* the approval cards are the chat's own (.aic-appr); here they only need air */
.attn-cards:not(:empty) { margin-bottom: 14px; }
.attn-cards .aic-wait-title { margin: 0 0 8px; font-weight: 600; color: var(--text); }
.attn-cards .aic-appr + .aic-appr { margin-top: 10px; }
/* the list is Home's card, drawn inside a box that already is one */
.attn-list .home-attention { margin: 0; padding: 0; border: 0; background: none; box-shadow: none; }
.attn-list .home-attention > h3 { font-size: 13px; }
.attn-list .home-allclear { margin: 0; }
.attn-asof { margin: 10px 0 0; font-size: 12px; }
.attn-asof:empty { display: none; }
/* …and the same cards on AI → Connect and a project's AI agent page (pages/ai.js #mcpCards) */
.ai-con-wait:not(:empty) { margin-bottom: 16px; }
.ai-con-wait .aic-wait-title { margin: 0 0 8px; font-weight: 600; color: var(--text); }
/* A PHONE: the tools are on a row of their own under the title, starting from the
   reading edge — a box hung from the button's end edge would leave the screen on the
   other side. So it hangs from the page head instead (which is sticky, so it is what an
   absolute box inside it measures from) and takes the screen's width. The same width
   switch as the head's own stacking above (860px). */
@media (max-width: 860px) {
  .attn-menu { position: static; }
  .attn-pop { inset-inline: 16px; width: auto; top: calc(100% + 6px); max-height: min(62vh, 560px); }
  /* …AND THE LANGUAGE MENU, which hung from its button's end edge the same way and for
     longer: 210px of menu from a button 100px into the screen, so the flags and the
     start of every name were off it, and the Arabic and Bengali rows were blank
     (measured at 390px: −76…134 in English, 256…466 in Arabic). From the page head too,
     starting at the reading edge. In the head only: the sign-in screen and the first-run
     wizard place their own copy by rules of their own (.pre-auth-tools), left as they are. */
  .head-tools .lang-menu { position: static; }
  .head-tools .lang-pop { inset-inline: 16px auto; top: calc(100% + 6px); }
  /* …AND THE PROJECT SWITCHER'S BOX, the same way and for the same reason: its pill is the
     first thing in the row, so a box hung from the pill's end edge would start off the
     screen. From the page head, across the screen. */
  .head-tools .proj-menu { position: static; }
  .head-tools .proj-menu-pop { inset-inline: 16px; width: auto; }
  /* …and the tools row gives way instead of running past the screen: the project
     switcher has min-width 0 and is the one that shrinks (the bell made the row a
     fourth button wider — 190 + 4 × 34 + 4 × 8 is 358px, a 360px phone has 328). The
     running job's pill gives way with it (its name is cut by its own ellipsis): with the
     Pro badge in the row as well, a pill that kept its 150px left the switcher nothing. */
  .head-tools { flex: 0 1 auto; min-width: 0; max-width: 100%; }
  .head-tools .job-pill { min-width: 0; }
}
@media (prefers-reduced-motion: reduce) { .attn-pop { animation: none; } }
@media print { .attn-menu { display: none !important; } }
.jobstep-list { list-style: none; margin: 10px 0 4px; padding: 0; display: grid; gap: 6px; }
.jobstep { display: flex; align-items: baseline; gap: 9px; font-size: 13.5px; color: var(--muted); }
.jobstep.ok { color: var(--text); }
.jobstep.running { color: var(--text); font-weight: 600; }
.jobstep.failed { color: var(--err-fg); }
.jobstep-mark { flex: 0 0 16px; text-align: center; }
.jobstep-mark .job-pill-dot, .jobstep .job-pill-dot { display: inline-block; }
/* the drawer takes the finished job's verdict as a coloured edge + tinted
   chip row — "did it work?" answered before a single log line is read */
.drawer.done-ok .drawer-bar { box-shadow: inset 0 3px 0 var(--ok); }
.drawer.done-err .drawer-bar { box-shadow: inset 0 3px 0 var(--err); }
/* ================= Activity page ========================================= */
.act-filters { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
.act-filters .field { min-width: 170px; flex: 0 1 auto; }
.act-filters .act-search { flex: 1 1 220px; }
/* on a phone the three filters each took their own width (170px or more) and stacked as
   ragged lines; here two share a line and the third fills its own, all edges aligned */
@media (max-width: 600px) { .act-filters .field { flex: 1 1 140px; min-width: 0; } }
.act-tbl td { vertical-align: top; padding-top: 10px; padding-bottom: 10px; }
.act-when { white-space: nowrap; color: var(--muted); width: 90px; }
.act-who { width: 92px; }
.act-tags { width: 170px; }
.act-tags .chip { margin: 0 4px 4px 0; }
.act-action { font-weight: 600; font-size: 13.5px; }
.act-detail { margin-top: 2px; overflow-wrap: anywhere; }
/* under an audit row's sentence, the panel's own id for what was done: the small part */
.act-detail.act-id { font-size: 12px; }
@media (max-width: 720px) { .act-tags { display: none; } .act-when { width: 70px; } }
/* ON A PHONE A ROW IS A CARD, not four columns. At 390px the when / who / log cells
   left the text column ~110px, so "cli-socket" broke mid-word and every detail ran
   one word per line (1 Oct 2026 walk, Activity, Arabic and English alike). The row
   wraps instead: when, who and the tags on top, the action and its detail below with
   the log button beside them. */
@media (max-width: 600px) {
  .act-tbl, .act-tbl tbody { display: block; }
  .act-tbl tr { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; padding: 10px 0; border-bottom: 1px solid var(--border); }
  .act-tbl td { display: block; padding: 0; border: 0; width: auto; }
  .act-tbl .act-when { order: 1; width: auto; font-size: 12.5px; }
  .act-tbl .act-who { order: 2; width: auto; }
  .act-tbl .act-tags { order: 3; display: flex; flex-wrap: wrap; gap: 4px; }
  .act-tbl .act-tags .chip { margin: 0; }
  .act-tbl .act-text { order: 4; flex: 1 1 0; min-width: 60%; }
  .act-tbl td.actions { order: 5; align-self: flex-start; position: static; background: none; }
  .act-tbl td.actions:empty { display: none; }
}

/* ---- Build with AI: the chat that builds the site, beside its live preview ---- */
.ai-msg { border-radius: 12px; padding: 10px 12px; font-size: 14px; line-height: 1.5; max-width: 92%; }
.ai-msg p { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.ai-user { align-self: flex-end; background: var(--accent-soft); color: var(--text); }
.ai-bot { align-self: flex-start; background: var(--well); color: var(--text); }
.ai-sys { align-self: stretch; max-width: none; border: 1px dashed var(--border-strong); display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.ai-sys p { flex: 1 1 240px; color: var(--muted); }
.ai-who { font-size: 12px; font-weight: 600; color: var(--muted); margin-bottom: 4px; }
.ai-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 12px; margin-top: 8px; }
.ai-meta:empty { display: none; }
.ai-meta summary { cursor: pointer; color: var(--muted); }
.ai-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; flex-basis: 100%; }
.ai-chip { border: 1px solid var(--border-strong); background: var(--surface); color: var(--text); border-radius: 999px; padding: 5px 11px; font: inherit; font-size: 12px; cursor: pointer; text-align: start; }
.ai-chip:hover, .ai-chip:focus-visible { border-color: var(--accent); color: var(--accent); outline: none; }
.ai-atts { display: flex; flex-wrap: wrap; gap: 6px; }
.ai-atts:empty { display: none; }
.ai-msg .ai-atts { margin-top: 8px; }
.ai-thumb { width: 72px; height: 72px; object-fit: cover; border-radius: 8px; border: 1px solid var(--border); display: block; background: var(--surface); }
.ai-thumb.sm { width: 26px; height: 26px; border-radius: 6px; }
.ai-file, .ai-pend { display: inline-flex; align-items: center; gap: 6px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 4px 8px; font-size: 12px; max-width: 240px; }
.ai-pend span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ai-x { border: 0; background: none; color: var(--muted); cursor: pointer; font-size: 16px; line-height: 1; padding: 0 2px; }
.ai-x:hover { color: var(--err); }
.ai-composer { border: 1px solid var(--border-strong); border-radius: 12px; padding: 8px; display: flex; flex-direction: column; gap: 8px; background: var(--surface); transition: box-shadow var(--tick), border-color var(--tick); }
.ai-composer:focus-within { border-color: var(--accent); }
.ai-composer.drag { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.ai-composer textarea { border: 0; box-shadow: none; resize: vertical; min-height: 64px; padding: 4px; background: transparent; }
.ai-composer textarea:focus { box-shadow: none; }
.ai-composer .btn-row { align-items: center; }
.ai-composer .btn svg { vertical-align: -3px; }
.ai-drop-hint { font-size: 12px; }
@media (max-width: 640px) { .ai-drop-hint { display: none; } }
.ai-frame { margin-top: 10px; border: 1px solid var(--border); border-radius: 10px; background: var(--well); overflow: auto; height: 74vh; display: flex; justify-content: center; }
.ai-frame iframe { width: 100%; height: 100%; border: 0; background: #fff; }
.ai-frame iframe[data-dev="tablet"] { width: 834px; max-width: 100%; }
.ai-frame iframe[data-dev="mobile"] { width: 390px; max-width: 100%; }
/* the bar (Publish, git, History, Speed, model), then chat | preview at the height of
   the window, each scrolling on its own; a phone shows one of the two, by tab */
.aib { display: flex; flex-direction: column; gap: 12px; }
.aib-top { display: flex; align-items: center; justify-content: space-between; gap: 8px 14px; flex-wrap: wrap; padding: 10px 14px; }
.aib-top-l, .aib-top-r { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
.aib-top .btn svg { vertical-align: -3px; }
.aib-pub { font-size: 13px; color: var(--muted); display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.aib-pub a { color: var(--accent); }
.aib-model { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; min-width: 0; }
.aib-model .chip { max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.aib-gitchip span { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.aib-gitchip.bad { color: var(--err-fg); border-color: var(--err); }
.seg.aib-tabs { display: none; align-self: flex-start; }
.aib-main { display: grid; grid-template-columns: minmax(340px, 5fr) 7fr; gap: 16px; height: calc(100dvh - 250px); min-height: 520px; }
.aib-chat { display: flex; flex-direction: column; gap: 10px; padding: 14px; min-height: 0; min-width: 0; }
.aib-thread { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: flex; flex-direction: column; gap: 10px; padding: 2px; }
.aib-col { display: flex; flex-direction: column; gap: 10px; }
.aib-col:empty { display: none; }
.aib-preview { display: flex; flex-direction: column; padding: 14px; min-height: 0; min-width: 0; }
.aib-preview .ai-frame { flex: 1; height: auto; min-height: 0; }
.aib-pv-bar { align-items: center; }
.aib-dev { display: inline-flex; align-items: center; gap: 6px; }
.aib-card { margin-top: 10px; border: 1px dashed var(--border-strong); border-radius: 10px; padding: 10px 12px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 13px; }
.aib-card p { flex: 1 1 260px; margin: 0; color: var(--muted); }
.aib-livehint, .aib-sleep { margin: 8px 0 0; font-size: 12.5px; }
/* a turn: Tia's words, a row per file she read or wrote (its diff folded inside), notes */
.aib-turn { align-self: stretch; max-width: 100%; }
.aib-turn.undone .aib-text, .aib-turn.undone .aib-tool { opacity: .6; }
.aib-text { margin: 4px 0; overflow-wrap: anywhere; }
.aib-tool { font-size: 12.5px; border-radius: 7px; margin: 2px 0; }
.aib-tool > summary, div.aib-tool { display: flex; align-items: center; gap: 7px; padding: 3px 6px; min-width: 0; }
details.aib-tool > summary { cursor: pointer; list-style: none; }
details.aib-tool > summary::-webkit-details-marker { display: none; }
details.aib-tool > summary::after { content: ''; flex: none; width: 5px; height: 5px; margin-inline: 4px 2px; border: solid var(--muted); border-width: 0 1.5px 1.5px 0; transform: rotate(-45deg); transition: transform var(--tick); }
details.aib-tool[open] > summary::after { transform: rotate(45deg); }
[dir="rtl"] details.aib-tool:not([open]) > summary::after { transform: rotate(135deg); }
details.aib-tool > summary:hover, details.aib-tool[open] > summary { background: color-mix(in srgb, var(--muted) 10%, transparent); border-radius: 7px; }
.aib-tool b { font-weight: 600; flex: none; }
.aib-tool code { direction: ltr; unicode-bidi: isolate; font-family: var(--mono); font-size: 12px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.aib-meta { margin-inline-start: auto; padding-inline-start: 8px; color: var(--muted); font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.aib-add { color: var(--ok-fg); }
.aib-del { color: var(--err-fg); }
.aib-ic { width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--muted); }
.aib-ic.ok { background: var(--ok); }
.aib-ic.err { background: var(--err); }
.aib-ic.run { background: var(--accent); animation: aib-pulse 1s ease-in-out infinite; }
@keyframes aib-pulse { 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) { .aib-ic.run { animation: none; } }
.aib-diff { direction: ltr; text-align: left; margin: 4px 0 6px; max-height: 340px; overflow: auto; font-family: var(--mono); font-size: 11.5px; line-height: 1.45; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 6px 8px; white-space: pre; tab-size: 2; }
.aib-diff .add { color: var(--ok-fg); background: var(--ok-soft); }
.aib-diff .del { color: var(--err-fg); background: var(--err-soft); }
.aib-diff .hd { color: var(--muted); }
.aib-note { font-size: 12px; color: var(--muted); font-style: italic; margin: 3px 0; padding-inline-start: 6px; }
.aib-typing { font-size: 13px; color: var(--muted); }
.aib-check, .aib-deverr { margin-top: 6px; }
.aib-check > summary, .aib-deverr > summary { cursor: pointer; font-size: 12.5px; color: var(--err-fg); }
.aib-deverr { margin-top: 10px; }
.aib-undo-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 12.5px; color: var(--muted); padding: 2px 6px; }
.aib-undo-row.undone { opacity: .6; }
.aib-gitcard { border-style: solid; border-color: var(--accent); }
/* History: one card per commit, its diff on request */
.aib-hist { display: flex; flex-direction: column; gap: 10px; max-height: 62vh; overflow-y: auto; margin: 8px 0; }
.aib-hist-row { border: 1px solid var(--border); border-radius: 10px; padding: 8px 10px; display: flex; flex-direction: column; gap: 4px; }
.aib-hist-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 13px; }
.aib-hist-head b { overflow-wrap: anywhere; }
.aib-joblog { max-height: 62vh; }
@media (max-width: 1000px) {
  .seg.aib-tabs { display: inline-flex; }
  .aib-main { grid-template-columns: minmax(0, 1fr); height: calc(100dvh - 230px); min-height: 460px; }
  .aib-main[data-tab="chat"] .aib-preview, .aib-main[data-tab="preview"] .aib-chat { display: none; }
}
/* …UNTIL AN AI PROVIDER IS SET UP. Then the thread holds a form, not a conversation: the
   ways to connect a provider and what tells them apart, taller than the window's share.
   Scrolled inside the card it was cut off at the message box in every language — at a
   desk the first way's lower half lay behind the thread's edge, and a press where its
   centre would be landed in the message box (the UI audit's COVERED, in Spanish, French,
   Portuguese, Indonesian and Bengali; in English the same way was cut at the same place
   and its centre fell on the 10 px between the two). So while the set-up is in the thread
   the two heights above are a floor and not a size: the card is as tall as the set-up, the
   message box comes after its last line, and it is the page that scrolls. The thread is
   then no scroll area of its own — one that cannot scroll still keeps the wheel
   (overscroll-behavior: contain), and the page would stand still under the pointer. */
.aib-main:has(.ai-setup-guide) { height: auto; min-height: max(520px, calc(100dvh - 250px)); }
.aib-main:has(.ai-setup-guide) .aib-thread { overflow: visible; }
@media (max-width: 1000px) { .aib-main:has(.ai-setup-guide) { min-height: max(460px, calc(100dvh - 230px)); } }
.seg { display: inline-flex; border: 1px solid var(--border-strong); border-radius: var(--r-ctl); overflow: hidden; }
.seg button { border: 0; background: var(--surface); color: var(--muted); padding: 5px 11px; font: inherit; font-size: 12px; cursor: pointer; }
.seg button + button { border-inline-start: 1px solid var(--border-strong); }
.seg button.on { background: var(--accent-soft); color: var(--accent); font-weight: 600; }

/* ===========================================================================
   SixPanel's own AI — the assistant (js/pages/assistant.js) and the AI page
   (js/pages/ai.js). Logical properties throughout, so Arabic mirrors itself.
   ======================================================================== */

/* min-width:0 all the way down: a flex item defaults to its content's width, and one wide
   table in an answer pushed the whole chat past a 390 px phone */
.aic { display: flex; flex-direction: column; min-width: 0; min-height: 0; height: 100%; }
.aic-main { position: relative; flex: 1; min-width: 0; min-height: 0; display: flex; }
.aic-log {
  flex: 1; min-width: 0; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 6px 4px 14px;
  display: flex; flex-direction: column; gap: 14px; overscroll-behavior: contain;
}
.aic-msg { display: flex; gap: 10px; min-width: 0; }
.aic-msg.user { justify-content: flex-end; }
.aic-bubble { font-size: 13.5px; line-height: 1.6; overflow-wrap: anywhere; min-width: 0; }
/* the owner's words: a soft bubble on the end side; Tia's: plain text beside her mark,
   full width — a reply with a table or a list reads like a page, not a speech balloon */
.aic-msg.user .aic-bubble {
  max-width: min(640px, 85%); padding: 9px 14px; border-radius: 18px; border-end-end-radius: 6px;
  background: var(--accent-soft); color: var(--text);
}
.aic-av {
  flex: none; width: 26px; height: 26px; border-radius: 50%; display: inline-grid; place-items: center;
  color: var(--on-solid); background: var(--grad); box-shadow: 0 2px 8px color-mix(in srgb, var(--accent) 25%, transparent);
}
.aic-av svg { display: block; }
.aic-msg.bot .aic-av { margin-top: 1px; }
.aic-body { flex: 1; min-width: 0; }
.aic-msg.bot .aic-bubble { color: var(--text); padding-top: 3px; }
.aic-acts { display: flex; gap: 2px; margin-top: 2px; opacity: 0; transition: opacity var(--tick); }
.aic-msg.bot:hover .aic-acts, .aic-acts:focus-within { opacity: 1; }
.aic-msg.live .aic-acts { display: none; }
@media (hover: none) { .aic-acts { opacity: 1; } }
.aic-act {
  display: inline-grid; place-items: center; width: 28px; height: 28px; border: 0; border-radius: 8px;
  background: none; color: var(--muted); cursor: pointer; padding: 0;
}
.aic-act:hover { background: var(--well); color: var(--text); }
.aic-act.done { color: var(--ok); }
.aic-act:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.aic-bubble.md p, .md p { margin: 0 0 8px; }
.aic-bubble.md p:last-child, .md p:last-child { margin-bottom: 0; }
.aic-bubble.md ul, .aic-bubble.md ol, .md ul, .md ol { margin: 4px 0 8px; padding-inline-start: 20px; }
.aic-bubble.md li { margin: 2px 0; }
.aic-bubble.md h4, .md h4 { margin: 12px 0 6px; font-size: 14px; }
.aic-bubble.md h4:first-child { margin-top: 0; }
.aic-bubble code, .md code { font-family: var(--mono); font-size: 12px; background: color-mix(in srgb, var(--muted) 14%, transparent); padding: 1px 5px; border-radius: 5px; }
.aic-bubble .tbl-wrap { margin: 8px 0; }
.aic-bubble a, .md a { color: var(--accent); }
.aic-code {
  margin: 6px 0; padding: 10px 12px; border-radius: 10px; overflow: auto; max-height: 360px;
  background: var(--console-bg); color: var(--console-text); font: 12px/1.5 var(--mono); white-space: pre-wrap; word-break: break-word;
}
.aic-code code { background: none; padding: 0; color: inherit; }
/* Tia at work: three dots, the word beside them for readers who do not read dots */
.aic-typing { display: inline-flex; align-items: center; gap: 4px; color: var(--muted); font-size: 12.5px; padding: 4px 0; }
.aic-typing i { width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: .35; animation: aic-bounce 1.2s infinite ease-in-out; }
.aic-typing i:nth-child(2) { animation-delay: .15s; }
.aic-typing i:nth-child(3) { animation-delay: .3s; }
.aic-typing-t { margin-inline-start: 6px; }
@keyframes aic-bounce { 0%, 80%, 100% { opacity: .25; transform: translateY(0); } 40% { opacity: .9; transform: translateY(-3px); } }
.aic-spin {
  display: inline-block; width: 12px; height: 12px; border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--accent) 25%, transparent); border-top-color: var(--accent);
  animation: aic-spin .7s linear infinite;
}
@keyframes aic-spin { to { transform: rotate(360deg); } }
/* the ↓ that brings the reader back to the newest words */
.aic-jump {
  position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%); z-index: 2;
  width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; cursor: pointer;
  border: 1px solid var(--border-strong); background: var(--surface); color: var(--text); box-shadow: var(--shadow-1);
}
.aic-jump:hover { color: var(--accent); border-color: var(--accent); }

/* the steps Tia took, folded into one line that opens to the rows */
.aic-steps { margin-inline-start: 36px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface-2); min-width: 0; }
.aic-steps > summary {
  list-style: none; cursor: pointer; display: flex; align-items: center; gap: 8px; padding: 7px 12px;
  font-size: 12.5px; color: var(--muted); min-width: 0;
}
.aic-steps > summary::-webkit-details-marker { display: none; }
.aic-steps > summary::after { content: ""; margin-inline-start: auto; flex: none; width: 7px; height: 7px; border-inline-end: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor; transform: rotate(45deg); transition: transform var(--tick); }
.aic-steps[open] > summary::after { transform: rotate(-135deg); }
.aic-steps > summary:hover { color: var(--text); }
.aic-steps > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 12px; }
.aic-steps-n { color: var(--text); font-weight: 600; flex: none; }
.aic-steps-names { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.aic-steps-mark, .aic-tool-mark { flex: none; display: inline-grid; place-items: center; width: 16px; height: 16px; color: var(--ok); }
.aic-steps.bad .aic-steps-mark { color: var(--err); }
.aic-steps-list { display: flex; flex-direction: column; gap: 6px; padding: 2px 12px 10px; border-top: 1px solid var(--border); padding-top: 8px; }
.aic-tool { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; font-size: 12.5px; min-width: 0; }
.aic-tool-name { font-weight: 600; color: var(--text); overflow-wrap: anywhere; }
.aic-tool.st-error .aic-tool-mark, .aic-tool.st-declined .aic-tool-mark { color: var(--err); }
.aic-tool-st { font-size: 11.5px; }
.aic-tool .btn.sm { min-height: 26px; padding: 0 9px; }
.aic-tool-more { flex-basis: 100%; }
.aic-tool-more summary { cursor: pointer; font-size: 12px; color: var(--muted); }
.aic-tool-line:empty { display: none; }
.aic-tool-line { flex-basis: 100%; font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-inline-start: 24px; }
.aic-note { margin: 0; }

.aic-appr, .aic-secret {
  border: 1px solid var(--border-strong); border-radius: 12px; padding: 12px 14px; background: var(--surface);
  display: flex; flex-direction: column; gap: 6px;
}
.aic-appr.danger { border-color: color-mix(in srgb, var(--warn) 60%, var(--border)); }
.aic-appr.done { opacity: .78; }
.aic-appr-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
.aic-appr-sum { margin: 2px 0; font-weight: 600; white-space: pre-wrap; overflow-wrap: anywhere; }
.aic-appr p.hint { margin: 0; }
.aic-appr details summary { cursor: pointer; font-size: 12.5px; color: var(--muted); }
.aic-appr .field { margin-bottom: 8px; }
.aic-appr-done { margin: 0; }
.aic-diff { margin: 6px 0; padding: 8px 10px; border-radius: 8px; background: var(--console-bg); color: var(--console-text); font: 12px/1.5 var(--mono); max-height: 320px; overflow: auto; white-space: pre; }
.aic-diff .add { color: #4ade80; }
.aic-diff .del { color: #f87171; }
.aic-secret input { max-width: 420px; }

.aic-foot { padding-top: 8px; display: flex; flex-direction: column; gap: 8px; }
.aic-sugg { display: flex; flex-wrap: wrap; gap: 6px; }
.aic-sugg:empty { display: none; }
.aic-form { margin: 0; }
.aic-box {
  display: flex; align-items: flex-end; gap: 6px; padding: 6px 6px 6px 14px; padding-inline: 14px 6px;
  border: 1px solid var(--border-strong); border-radius: 18px; background: var(--surface);
  box-shadow: var(--shadow-1); transition: border-color var(--tick), box-shadow var(--tick);
}
.aic-box:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.aic-box textarea {
  flex: 1; min-width: 0; resize: none; border: 0; outline: 0; background: none; color: var(--text);
  font: 13.5px/1.5 var(--font); padding: 6px 0; min-height: 32px; max-height: 180px;
}
.aic-box textarea::placeholder { color: var(--muted); }
/* the box draws the focus ring; the global textarea:focus one inside it was a second border */
.aic-box textarea:focus, .aic-box textarea:focus-visible { box-shadow: none; border: 0; outline: 0; }
.aic-send {
  flex: none; width: 34px; height: 34px; border-radius: 50%; border: 0; cursor: pointer; display: grid; place-items: center;
  background: var(--accent); color: var(--accent-fg); transition: background var(--tick), opacity var(--tick), transform var(--tick);
}
.aic-send:hover { background: var(--accent-strong); }
.aic-send:active { transform: scale(.94); }
.aic-send:focus-visible { outline: 3px solid var(--accent-soft); outline-offset: 1px; }
.aic-form.empty .aic-send:not(.stop) { opacity: .45; }
.aic-send.stop { background: var(--text); color: var(--surface); }
.aic-send svg { display: block; }
.aic-meta { display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center; margin: 0; font-size: 11.5px; padding-inline: 6px; }
.aic-meta .chip { font-size: 11px; }
.aic-meta:empty { display: none; }
.aic-hello { margin: auto 0; padding: 18px 6px; text-align: center; display: flex; flex-direction: column; align-items: center; }
.aic-hello-av { width: 44px; height: 44px; border-radius: 14px; display: grid; place-items: center; color: var(--on-solid); background: var(--grad); box-shadow: 0 8px 24px color-mix(in srgb, var(--accent) 30%, transparent); margin-bottom: 10px; }
.aic-hello h3 { margin: 0 0 4px; font-size: 17px; }
.aic-hello > .hint { margin: 0; max-width: 520px; font-size: 12.5px; line-height: 1.5; }
.aic-starters { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin-top: 14px; max-width: 640px; }
.aic-chip {
  border: 1px solid var(--border-strong); background: var(--surface); color: var(--text); border-radius: 999px;
  padding: 6px 12px; font: 12.5px/1.35 var(--font); cursor: pointer; text-align: start; max-width: 100%; overflow-wrap: anywhere;
  transition: border-color var(--tick), color var(--tick), background var(--tick);
}
.aic-chip:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.aic-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ai-warn-note { color: var(--warn-fg); background: var(--warn-soft); border-radius: 10px; padding: 8px 12px; }

/* the AI page's Assistant tab */
.ai-chat-page { display: grid; grid-template-columns: 264px minmax(0, 1fr); gap: 16px; align-items: stretch; height: calc(100dvh - 190px); min-height: 480px; }
.ai-chat-page.folded { grid-template-columns: minmax(0, 1fr); }
.ai-chat-page.folded .ai-chats { display: none; }
/* the conversation list: a quiet column, no card around it */
.ai-chats { display: flex; flex-direction: column; gap: 8px; min-height: 0; padding-inline-end: 12px; border-inline-end: 1px solid var(--border); }
.ai-chats-head { display: flex; align-items: center; gap: 6px; }
.ai-chats-new { flex: 1; justify-content: center; gap: 6px; }
.ai-chats-search { height: 34px; font-size: 13px; }
.ai-chats-list { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: flex; flex-direction: column; gap: 1px; padding-bottom: 8px; }
.ai-chats-h { margin: 12px 8px 4px; font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
.ai-chats-h:first-child { margin-top: 4px; }
.ai-chats-note { margin: 10px 8px; font-size: 12.5px; }
.ai-chat-item { position: relative; display: flex; align-items: center; border-radius: 9px; min-width: 0; }
.ai-chat-item:hover { background: var(--well); }
.ai-chat-item.active { background: var(--accent-soft); }
.ai-chat-open {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 7px; text-align: start; background: none; border: 0;
  color: var(--text); cursor: pointer; padding: 7px 10px; font: inherit; font-size: 13px; border-radius: 9px;
}
.ai-chat-open:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.ai-chat-item.active .ai-chat-open { font-weight: 600; }
.ai-chat-t { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ai-chat-acts { display: none; gap: 0; padding-inline-end: 4px; flex: none; }
.ai-chat-item:hover .ai-chat-acts, .ai-chat-item:focus-within .ai-chat-acts, .ai-chat-item.active .ai-chat-acts { display: inline-flex; }
@media (hover: none) { .ai-chat-acts { display: inline-flex; } }
.ai-chat-acts .aic-act { width: 26px; height: 26px; }
.ai-chat-item.renaming { padding: 2px; }
.ai-chat-item.renaming .input { height: 32px; font-size: 13px; }
.ai-chat-main { min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 10px; }
.ai-chats-show { align-self: flex-start; gap: 6px; }
.ai-chat-host { flex: 1; min-height: 0; display: flex; }
.ai-chat-host > .aic { flex: 1; min-width: 0; }
.ai-wait:empty { display: none; }
.ai-wait .card { margin-bottom: 0; }
.ai-wait .aic-appr + .aic-appr { margin-top: 10px; }
@media (max-width: 860px) {
  .ai-chat-page { grid-template-columns: 1fr; height: calc(100dvh - 160px); min-height: 400px; }
  .ai-chat-page:not(.folded) .ai-chat-main { display: none; }
  .ai-chats { padding-inline-end: 0; border-inline-end: 0; }
}

/* adding a provider (AiSetup, js/pages/ai.js): a card per provider, then the key, the
   model and Save and check — the same block on the Providers tab, in the assistant's empty
   conversation, in the dock and on Build with AI, so it has to read at 460 px too */
.ai-card-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
.ai-card-head h3 { margin: 0; }
.ai-sub { margin: 16px 0 8px; font-size: 13px; font-weight: 650; color: var(--text); }
/* a choice-row's heading: .field label's look, without .field's rule reaching the cards
   (every <label> in a .field is display:block — a choice card inside one lost its grid) */
.ai-block { margin-bottom: 14px; }
.ai-label { color: var(--muted); font-size: 12.5px; margin: 0 0 6px; font-weight: 600; }
.ai-block .choice-row { margin-top: 0; }
.ai-setup-intro { margin: 0 0 10px; color: var(--muted); font-size: 13px; }
.ai-presets { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 8px; }
.ai-setup.compact .ai-presets { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.choice-card.ai-preset { padding: 10px 12px; border-radius: 12px; gap: 2px; }
.choice-card.ai-preset .choice-title { font-size: 13.5px; }
.choice-card.ai-preset .choice-why { font-size: 12px; line-height: 1.4; }
/* STEP 1 — HOW — IS THE DECISION, so its two or three choices are big and plain: a name, one
   line, and what it works for as chips (a plan's two beside a key's four is the difference,
   seen before a word is read). Two to a row where there is room, one under the other on a
   phone and in a narrow mount. Written BELOW .ai-presets and its compact rule: of the same
   weight, the later one wins. */
.ai-hows { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; max-width: 860px; }
@media (max-width: 640px) { .ai-hows { grid-template-columns: 1fr; } }
.ai-setup.compact .ai-hows { grid-template-columns: 1fr; }
.choice-card.ai-how { padding: 14px 16px; gap: 6px; align-content: start; }
.choice-card.ai-how .choice-title { font-size: 15px; }
.choice-card.ai-how .choice-why { font-size: 13px; line-height: 1.45; }
.ai-works { display: flex; flex-wrap: wrap; align-items: center; gap: 5px 6px; margin-top: 4px; }
.ai-works-l { font-size: 12px; color: var(--muted); margin-inline-end: 2px; }
/* the one note among the ways (a model on this server is not offered): plain text in the cell
   a fourth way would take — no border, nothing to press */
.ai-setup-nolocal { margin: 0; padding: 0 4px; align-self: center; line-height: 1.5; }
/* "What is the difference?" — a plan beside a key, three rows. On a phone a row's name goes
   above its two answers: three columns of sentences at 300 px were three words a line. */
.ai-diff { margin: 14px 0 0; max-width: 860px; }
.ai-diff summary { cursor: pointer; font-size: 13px; font-weight: 650; color: var(--text); margin-bottom: 8px; }
.ai-diff-grid { display: grid; grid-template-columns: minmax(110px, max-content) 1fr 1fr; gap: 8px 16px; font-size: 13px; line-height: 1.45; border: 1px solid var(--border); border-radius: 12px; padding: 12px 14px; }
.ai-diff-grid b { font-weight: 650; }
.ai-diff-k { color: var(--muted); }
@media (max-width: 560px) {
  .ai-diff-grid { grid-template-columns: 1fr 1fr; gap: 4px 12px; }
  .ai-diff-corner { display: none; }
  .ai-diff-k { grid-column: 1 / -1; margin-top: 8px; font-size: 12px; }
}
.ai-setup.compact .ai-diff-grid { grid-template-columns: 1fr 1fr; gap: 4px 12px; }
.ai-setup.compact .ai-diff-corner { display: none; }
.ai-setup.compact .ai-diff-k { grid-column: 1 / -1; margin-top: 8px; font-size: 12px; }
/* a subscription's step 3: what it is for and what it costs in safety, THEN the button — each
   with room of its own (they ran into one another: a paragraph, the button, the fold) */
.ai-setup-signin { max-width: 760px; }
.ai-setup-signin > .hint { margin: 0 0 8px; }
.ai-signin-stage { margin: 16px 0 14px; }
.ai-signin-stage p { margin: 0 0 8px; }
.ai-signin-have summary { cursor: pointer; font-size: 12.5px; color: var(--muted); }
.ai-signin-have[open] summary { margin-bottom: 10px; }
.ai-signin-result:empty { display: none; }
.ai-signin-result { margin-top: 12px; }
.ai-plan-why { margin: 6px 0 0; }
.ai-setup-form { max-width: 760px; }
.ai-setup-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.ai-setup-head b { font-size: 14px; }
.ai-keylink { font-weight: 500; margin-inline-start: 8px; color: var(--accent); }
.ai-model-row { display: flex; gap: 8px; align-items: flex-start; }
.ai-model-row .combo { flex: 1; min-width: 0; }
.ai-model-note { margin: 6px 0 0; }
.ai-model-note:empty { display: none; }
.ai-model-note.ok { color: var(--ok-fg); }
.ai-model-note.err { color: var(--err-fg); }
.ai-model-note .btn { margin-inline-start: 4px; vertical-align: baseline; min-height: 24px; }
.ai-setup-more { margin: 2px 0 12px; }
.ai-setup-more summary { cursor: pointer; font-size: 12.5px; color: var(--muted); margin-bottom: 8px; }
.ai-setup-result:empty { display: none; }
.ai-setup-result { margin-top: 12px; }
.ai-setup-result .verdict { white-space: pre-wrap; overflow-wrap: anywhere; }
.ai-setup.compact .ai-setup-form .wz-field-row { grid-template-columns: 1fr; }
@media (max-width: 560px) { .ai-model-row { flex-direction: column; align-items: stretch; } }

/* the saved providers */
.ai-provs { display: flex; flex-direction: column; gap: 10px; }
.ai-prov { display: flex; gap: 12px; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; border: 1px solid var(--border); border-radius: 12px; padding: 12px 14px; }
.ai-prov.active { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
.ai-prov-main { min-width: 0; flex: 1 1 320px; display: flex; flex-direction: column; gap: 3px; }
.ai-prov-main .hint { margin: 0; }
.ai-prov-check { margin-top: 4px; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; font-size: 12.5px; }
.ai-prov-err { flex-basis: 100%; color: var(--err-fg); font-size: 12.5px; overflow-wrap: anywhere; }
.ai-prov-actions { justify-content: flex-end; }

/* Monitoring: what it watches, its switches, where alerts go and what was sent */
.ai-mon-status { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
.ai-mon-off { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; justify-content: space-between; margin-top: 12px; }
.ai-watch td { vertical-align: top; }
.ai-watch-hosts { margin-top: 2px; font-size: 11.5px; overflow-wrap: anywhere; }
/* a line breaks between two hostnames, not inside one (beta-↵spa.allsweb.net) —
   unless one alone is wider than the line, which then wraps rather than overflow */
.ai-watch-hosts span { display: inline-block; max-width: 100%; }
.ai-mon-switches .check { margin: 7px 0; }
.ai-mon-need { margin: -2px 0 6px 46px; }
.ai-hour { width: auto; min-height: 30px; margin-inline-start: 6px; }
.ai-mon-ai { margin-top: 10px; }
.ai-mon-ai summary { cursor: pointer; font-size: 13px; color: var(--muted); margin-bottom: 10px; }
.ai-mon-ai .btn-row { align-self: end; margin-bottom: 14px; }
.ai-ch-head { display: flex; align-items: center; gap: 8px; margin: 12px 0 4px; }
/* a destination row: a long status chip or a channel's buttons wrap under its name on a phone */
.ai-dest { flex-wrap: wrap; align-items: center; }
.ai-dest > span:first-child { min-width: 0; flex: 1 1 220px; }
.ai-dest .chip { white-space: normal; }
.ai-ch-form { border: 1px solid var(--border); border-radius: 12px; padding: 12px 14px; margin: 10px 0; }
.ai-sent { list-style: none; margin: 0; padding: 0; }
.ai-sent li { padding: 8px 0; border-bottom: 1px solid var(--border); }
.ai-sent li:last-child { border-bottom: 0; }
.ai-sent-head { font-size: 13px; overflow-wrap: anywhere; }
.ai-sent-to { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
.ai-sent-err { margin: 4px 0 0; color: var(--err-fg); overflow-wrap: anywhere; }

/* Connect: three numbered steps — the agent, what to paste, its first call */
.ai-step + .ai-step { border-top: 1px solid var(--border); margin-top: 14px; }
.ai-step > .ai-sub { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
.ai-n { flex: none; display: inline-grid; place-items: center; width: 22px; height: 22px; border-radius: 50%; background: var(--well); border: 1px solid var(--border-strong); font-size: 12px; }
.ai-do { margin: 10px 0 0; padding-inline-start: 22px; display: grid; gap: 10px; font-size: 13px; }
.ai-do .copy-row { margin-top: 6px; }
.ai-mcp-out .ai-label { margin: 10px 0 4px; }
.ai-clients { display: flex; flex-wrap: wrap; gap: 6px; }
.ai-clients .btn { border-color: var(--border-strong); }
.choice-card.disabled { opacity: .6; cursor: not-allowed; }
.ai-mcp-out { margin-top: 4px; }
.ai-mcp-out .verdict { margin-bottom: 10px; }
#aiTokEdit .seg { flex-wrap: wrap; }
#aiTokEdit > .hint { margin: 0 0 12px; }
.ai-tok-projects { margin-inline-start: 24px; }
.seg.ai-seg-wrap { flex-wrap: wrap; }
/* three long choices do not fit one row on a phone: they stack instead of breaking mid-control */
@media (max-width: 560px) {
  .seg.ai-seg-wrap { display: flex; flex-direction: column; }
  .seg.ai-seg-wrap button { text-align: start; padding: 8px 12px; }
  .seg.ai-seg-wrap button + button { border-inline-start: 0; border-top: 1px solid var(--border-strong); }
}

/* the conversation's own additions: the full check-up, the setup in place of the chat */
.aic-checkup { margin-top: 16px; width: 100%; max-width: 440px; }
.aic-checkup-btn {
  width: 100%; display: flex; align-items: center; gap: 12px; text-align: start; cursor: pointer; padding: 12px 14px;
  border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border)); border-radius: 14px;
  background: linear-gradient(135deg, var(--accent-soft), var(--surface)); color: var(--text); font: inherit;
  transition: border-color var(--tick), transform var(--tick);
}
.aic-checkup-btn:hover { border-color: var(--accent); }
.aic-checkup-btn:active { transform: scale(.99); }
.aic-checkup-btn:focus-visible { outline: 3px solid var(--accent-soft); outline-offset: 1px; }
.aic-checkup-btn b { display: block; font-size: 13.5px; }
.aic-checkup-btn .hint { display: block; margin: 2px 0 0; font-size: 12px; line-height: 1.4; }
.aic-checkup-ic { flex: none; width: 36px; height: 36px; border-radius: 10px; display: grid; place-items: center; background: var(--accent); color: var(--accent-fg); }
.aic-setup { padding: 8px 2px; }
.aic-setup h3 { margin-bottom: 4px; }
.aic-sugg-label { flex-basis: 100%; margin: 0; font-size: 12px; }
.aic-who { color: inherit; text-decoration: none; }
.aic-who:hover { color: var(--accent); }
.aic-bubble.md li > ul, .aic-bubble.md li > ol, .md li > ul, .md li > ol { margin: 4px 0 2px; }
/* The Tia tab is an app, not a page: the page's subtitle, the footer and the bottom padding
   step aside so the conversation gets the screen (the tab row stays). */
body:has(.ai-chat-page) #pageHint, body:has(.ai-chat-page) .app-foot { display: none; }
body:has(.ai-chat-page) .page-head { min-height: 0; margin-bottom: 14px; }
body:has(.ai-chat-page) .main { padding-bottom: 16px; }
body:has(.ai-chat-page) .tabbar { margin-bottom: 12px; }
.ai-chat-page { height: calc(100dvh - 150px); }
@media (max-width: 860px) { .ai-chat-page { height: calc(100dvh - 140px); } }

/* ---- Tia v2: the start screen, the box's switch, "/", code blocks, errors, the user's own actions ---- */
/* the AI page with an empty conversation: greeting and box in the middle, ideas below */
/* `safe`: when the greeting is taller than the screen (a phone) it starts at the top and
   scrolls, instead of being centred off the top edge under the button above it */
.aic.home.is-empty { justify-content: safe center; }
.aic.home.is-empty .aic-main { flex: 0 1 auto; }
.aic.home.is-empty .aic-log { padding-bottom: 6px; }
.aic.home.is-empty .aic-hello { margin: 0; padding-top: 0; }
.aic.home.is-empty .aic-foot { width: 100%; max-width: 720px; margin: 0 auto; }
.aic.home .aic-log, .aic.home .aic-foot { width: 100%; max-width: 820px; margin-inline: auto; }
.aic.home .aic-main { width: 100%; }
/* the streaming answer: finished part drawn once, the growing tail below it */
.aic-live-done:empty { display: none; }
/* code: a header with its language and Copy */
.aic-codeblock { margin: 8px 0; border-radius: 10px; overflow: hidden; background: var(--console-bg); }
.aic-code-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 5px 10px; font: 11px var(--mono); color: color-mix(in srgb, var(--console-text) 65%, transparent); border-bottom: 1px solid color-mix(in srgb, var(--console-text) 12%, transparent); }
.aic-code-copy { border: 0; background: none; color: inherit; font: inherit; cursor: pointer; padding: 2px 6px; border-radius: 6px; }
.aic-code-copy:hover { background: color-mix(in srgb, var(--console-text) 12%, transparent); color: var(--console-text); }
.aic-codeblock .aic-code { margin: 0; border-radius: 0; }
/* the newest answer offers Try again; only then */
.aic-retry { display: none; }
.aic-msg.bot.last .aic-retry { display: inline-grid; }
/* the owner's own message: Edit (the last one) and Save as a prompt, on hover */
.aic-ubody { display: flex; flex-direction: column; align-items: flex-end; min-width: 0; max-width: min(640px, 85%); }
.aic-ubody .aic-bubble { max-width: 100%; }
.aic-msg.user .aic-ubody .aic-bubble { max-width: 100%; }
.aic-uacts { justify-content: flex-end; }
.aic-msg.user:hover .aic-uacts, .aic-uacts:focus-within { opacity: 1; }
.aic-msg.user [data-editmsg] { display: none; }
.aic-msg.user.last [data-editmsg] { display: inline-grid; }
.aic.busy .aic-uacts { display: none; }
/* a failed turn, where it happened */
.aic-err { margin-inline-start: 36px; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; padding: 10px 12px; border-radius: 12px; border: 1px solid color-mix(in srgb, var(--err) 45%, var(--border)); background: color-mix(in srgb, var(--err) 7%, var(--surface)); font-size: 13px; min-width: 0; }
.aic-err b { flex-basis: 100%; color: var(--err); }
.aic-err span { flex: 1 1 200px; min-width: 0; overflow-wrap: anywhere; color: var(--text); }
.aic-err .btn { gap: 6px; }
/* Ask / Auto in the line under the box */
.aic-mode { display: inline-flex; border: 1px solid var(--border-strong); border-radius: 999px; padding: 2px; background: var(--surface); }
.aic-mode button { border: 0; background: none; color: var(--muted); font: inherit; font-size: 11.5px; font-weight: 600; padding: 2px 10px; border-radius: 999px; cursor: pointer; }
.aic-mode button.on { background: var(--accent-soft); color: var(--accent); }
.aic-mode button.on[data-mode="auto"] { background: var(--warn-soft); color: var(--warn-fg); }
.aic-mode button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* "/" — commands and saved prompts, over the box */
.aic-foot { position: relative; }
.aic-slash {
  position: absolute; inset-inline: 0; bottom: calc(100% - 4px); z-index: 5; max-height: 300px; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--border-strong); border-radius: 14px; box-shadow: var(--shadow-2); padding: 6px;
}
.aic-slash-h { margin: 6px 8px 4px; font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
.aic-slash-it { display: flex; align-items: baseline; gap: 10px; width: 100%; text-align: start; border: 0; background: none; color: var(--text); font: inherit; font-size: 13px; padding: 7px 10px; border-radius: 9px; cursor: pointer; min-width: 0; }
.aic-slash-it b { font-family: var(--mono); font-size: 12.5px; flex: none; color: var(--accent); }
.aic-slash-it span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }
.aic-slash-it .aic-slash-p { color: var(--text); }
.aic-slash-it.on, .aic-slash-it:hover { background: var(--accent-soft); }
.aic-slash-row { display: flex; align-items: center; gap: 2px; }
.aic-slash-row .aic-slash-it { flex: 1; }
.aic-slash-none { margin: 8px 10px; }
/* Automations */
.ai-au-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.ai-au-top > div { flex: 1 1 320px; min-width: 0; }
.ai-au-top .btn { gap: 6px; }
.ai-au-list { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; }
.ai-au { border: 1px solid var(--border); border-radius: 12px; padding: 12px 14px; min-width: 0; }
.ai-au.off { opacity: .7; }
.ai-au-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; min-width: 0; }
.ai-au-ic { flex: none; width: 32px; height: 32px; border-radius: 10px; display: grid; place-items: center; background: var(--accent-soft); color: var(--accent); }
.ai-au-main { flex: 1 1 220px; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ai-au-main .hint { font-size: 12px; overflow-wrap: anywhere; }
.ai-au-acts { display: inline-flex; flex-wrap: wrap; gap: 4px; }
.ai-au-acts .btn { gap: 5px; }
.ai-au-prompt { margin: 8px 0 6px; font-size: 13px; color: var(--text); white-space: pre-wrap; overflow-wrap: anywhere; }
.ai-au-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; font-size: 12px; }
.ai-au-result { margin-top: 6px; font-size: 13px; }
.ai-au-result summary { cursor: pointer; color: var(--muted); font-size: 12px; }
.ai-au-form { margin: 12px 0 0; border: 1px solid var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); display: flex; flex-direction: column; gap: 10px; }
.ai-au-form .check { display: block; font-size: 13px; }
.ai-au-when { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.ai-au-when .input { width: auto; min-width: 0; }
.ai-au-days { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 8px; }
.ai-au-days .check { display: inline-flex; }
.ai-au-err:not([hidden]) { color: var(--err); margin: 0; }
.ai-au-tpls { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px; }
.ai-au-tpl { text-align: start; border: 1px solid var(--border-strong); background: var(--surface); color: var(--text); border-radius: 12px; padding: 10px 12px; cursor: pointer; font: inherit; display: flex; flex-direction: column; gap: 3px; }
.ai-au-tpl:hover { border-color: var(--accent); background: var(--accent-soft); }
.ai-au-tpl b { font-size: 13px; }
.ai-au-tpl .hint { font-size: 12px; }
@media (prefers-reduced-motion: reduce) { .aic-spin { animation: none; } }

/* Build with AI: the provider setup inside the thread */
.ai-setup-guide { align-self: stretch; max-width: none; border: 1px dashed var(--border-strong); background: var(--surface); }

/* incidents */
.ai-inc { border: 1px solid var(--border); border-radius: 12px; padding: 12px 14px; margin-top: 10px; }
.ai-inc.open { border-color: var(--border-strong); }
.ai-inc-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* an incident quotes Health's own detail, paths and hostnames included — a
   /var/www/<proj>/react/next.config.js widened the phone page to 557px */
.ai-inc > p { margin: 6px 0; font-size: 13px; overflow-wrap: anywhere; }
.ai-inc-analysis { background: var(--well); border-radius: 10px; padding: 10px 12px; margin: 8px 0; font-size: 13px; overflow-wrap: anywhere; }
.ai-inc-analysis ol { margin: 4px 0; padding-inline-start: 20px; }
.ai-inc details { margin: 6px 0; }
.ai-inc details summary { cursor: pointer; font-size: 12.5px; color: var(--muted); }
.ai-inc-events { margin: 6px 0; padding-inline-start: 18px; font-size: 12.5px; }
.ai-inc .btn-row { margin-top: 8px; }

/* the dock: the assistant over any page */
.ai-dock-btn {
  position: fixed; inset-inline-end: 18px; bottom: 18px; z-index: 85;
  display: inline-flex; align-items: center; gap: 8px; padding: 0 18px 0 14px; height: 46px;
  border: 0; border-radius: 23px; cursor: pointer; font: 600 13.5px var(--font);
  color: var(--on-solid); background: var(--grad); box-shadow: 0 10px 28px color-mix(in srgb, var(--accent) 38%, transparent);
  transition: transform var(--tick), filter var(--tick);
}
.ai-dock-btn:hover { filter: brightness(1.06); transform: translateY(-1px); }
.ai-dock-btn:focus-visible { outline: 3px solid var(--accent-soft); outline-offset: 2px; }
.ai-dock-dot {
  position: absolute; top: -4px; inset-inline-end: -4px; min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: 9px; background: var(--err); color: var(--on-solid); font-size: 11px; line-height: 18px; text-align: center;
  box-shadow: 0 0 0 2px var(--surface);
}
.ai-dock-dot.warn { background: var(--warn); min-width: 10px; width: 10px; height: 10px; padding: 0; top: -2px; inset-inline-end: -2px; }
.ai-dock {
  position: fixed; inset-inline-end: 18px; bottom: 18px; z-index: 88;
  width: min(440px, calc(100vw - 36px)); height: min(660px, calc(100vh - 90px));
  display: flex; flex-direction: column; background: var(--surface);
  border: 1px solid var(--border); border-radius: 20px; box-shadow: var(--shadow-2); overflow: hidden;
  animation: ai-dock-in .18s ease-out; transform-origin: bottom right;
  transition: width .18s ease-out, height .18s ease-out;
}
[dir="rtl"] .ai-dock { transform-origin: bottom left; }
.ai-dock.large { width: min(760px, calc(100vw - 36px)); height: calc(100vh - 36px); }
@keyframes ai-dock-in { from { opacity: 0; transform: translateY(10px) scale(.98); } to { opacity: 1; transform: none; } }
.ai-dock-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 10px 10px 14px; padding-inline: 14px 10px; border-bottom: 1px solid var(--border); }
.ai-dock-who { display: inline-flex; align-items: center; gap: 10px; min-width: 0; }
.ai-dock-names { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.ai-dock-names b { font-size: 14px; }
.ai-dock-status { font-size: 11.5px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ai-dock-status.on { color: var(--accent); }
.ai-dock-actions { display: inline-flex; gap: 2px; flex: none; }
.aic-ib {
  display: inline-grid; place-items: center; width: 32px; height: 32px; border-radius: 9px; border: 0;
  background: none; color: var(--muted); cursor: pointer; padding: 0; text-decoration: none;
}
.aic-ib:hover { background: var(--well); color: var(--text); text-decoration: none; }
.aic-ib[aria-expanded="true"] { background: var(--accent-soft); color: var(--accent); }
.aic-ib:focus-visible { outline: 2px solid var(--accent); outline-offset: 0; }
.aic-ib svg { display: block; }
.ai-dock-recent {
  position: absolute; inset-inline: 10px; top: 58px; z-index: 5; max-height: 60%; overflow: auto;
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow-2); padding: 6px;
}
.ai-dock-recent-h { margin: 4px 8px 6px; font-size: 11.5px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.ai-dock-recent-note { margin: 8px; }
.ai-dock-chat {
  width: 100%; display: flex; align-items: baseline; gap: 10px; justify-content: space-between; text-align: start;
  border: 0; background: none; color: var(--text); cursor: pointer; padding: 8px 10px; border-radius: 9px; font: 13px var(--font);
}
.ai-dock-chat:hover { background: var(--well); }
.ai-dock-chat.active { background: var(--accent-soft); }
.ai-dock-chat-t { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ai-dock-chat .hint { flex: none; font-size: 11px; margin: 0; }
.ai-dock-wait { max-height: 40%; overflow: auto; padding: 0 12px; }
.ai-dock-wait:not(:empty) { padding-top: 10px; border-bottom: 1px solid var(--border); padding-bottom: 10px; }
.ai-dock-wait .aic-appr + .aic-appr { margin-top: 8px; }
.ai-dock-body { flex: 1; min-height: 0; display: flex; padding: 8px 12px 12px; }
.ai-dock-body > .aic { flex: 1; min-width: 0; }
.aic.compact .aic-bubble { font-size: 13px; }
.aic.compact .aic-msg.user .aic-bubble { max-width: 88%; }
.aic.compact .aic-steps { margin-inline-start: 0; }
@media (max-width: 600px) {
  .ai-dock, .ai-dock.large { inset: 0; width: 100vw; height: 100dvh; border-radius: 0; border: 0; animation: none; }
  .ai-dock-bar { padding-top: max(10px, env(safe-area-inset-top)); }
  .ai-dock-body { padding-bottom: max(12px, env(safe-area-inset-bottom)); }
  .ai-dock-recent { top: calc(58px + env(safe-area-inset-top)); }
  [data-dock="size"] { display: none; }
  .ai-dock-btn .ai-dock-label { display: none; }
  .ai-dock-btn { width: 50px; height: 50px; padding: 0; justify-content: center; border-radius: 50%; bottom: max(16px, env(safe-area-inset-bottom)); }
  .aic-steps { margin-inline-start: 0; }
  .aic-msg.bot .aic-av { display: none; }
}
@media print { .ai-dock-btn, .ai-dock { display: none !important; } }
@media (prefers-reduced-motion: reduce) {
  .aic-typing i, .aic-spin, .ai-dock { animation: none; }
  .ai-dock { transition: none; }
  .ai-dock-btn:hover { transform: none; }
}
/* a page with a sticky save bar (Environment) keeps its Save button reachable: the dock
   button rides above the bar instead of over it */
body:has(.savebar-sticky) .ai-dock-btn { bottom: 84px; }

/* ---- SixPanel Pro, the AllsWeb account and Addons (1.5.2) ----
   One look for the upgrade card wherever a Pro feature is offered (core.js proCardHtml)
   and for the addon card (addonNeedHtml): an accent edge, never a wall over the page. */
.pro-card, .addon-need { border-inline-start: 3px solid var(--accent); }
.addon-need { border-inline-start-color: var(--warn-fg); }
.addon-need.covered { border-inline-start-color: var(--info-fg); }
.pro-card-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; }
.pro-card-head h3 { margin: 0; }
.pro-card .hint, .addon-need .hint { margin: 4px 0 0; }
.pro-card .btn-row, .addon-need .btn-row { margin-top: 14px; }
.pro-card.compact { padding: 14px 18px; }
.pro-slot:empty { display: none; }
/* Pro on its 14-day trial: one line where the upgrade card would be (core.js proTrialNoteHtml) */
.pro-trial-note { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px;
  padding: 10px 14px; border: 1px solid var(--border); border-inline-start: 3px solid var(--accent); border-radius: 10px; }
.pro-trial-note .pro-trial-text { flex: 1 1 220px; min-width: 0; }
.pro-card .pro-trial-ended { color: var(--text); font-weight: 600; }
.kind-card .badge { align-self: center; flex: 0 0 auto; }
.acct-code { font: 700 28px/1.2 var(--mono); letter-spacing: .12em; margin: 10px 0; user-select: all; }
.acct-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin: 12px 0; }
.acct-cell { border: 1px solid var(--border); border-radius: 10px; padding: 10px 12px; min-width: 0; }
.acct-cell .k { color: var(--muted); font-size: 12px; }
.acct-cell .v { font-weight: 650; overflow-wrap: anywhere; }
.acct-plans { display: flex; gap: 6px; flex-wrap: wrap; }
.addon-row { display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap; }
.addon-row .addon-main { flex: 1 1 260px; min-width: 0; }

/* Server ▸ Database (js/pages/dbserver.js). The time-zone card carries a state
   banner INSIDE the card (inline-banner), a one-line answer, and the change row:
   label + select + button that wrap as one unit on a phone. */
.inline-banner { margin: 10px 0 12px; padding: 12px 14px; border-radius: 12px; box-shadow: none; }
.dbs-tz-now { margin: 8px 0 6px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
/* the last move sits under the two kv rows, not glued to the second one */
.dbs-tz-moved { margin-top: 10px; }
/* Server ▸ PHP on a phone: the "Used by" column (a link that opens the version's
   websites) sat past the screen's edge behind the sticky Manage cell — the same
   thing Manage opens. There the summary moves under the version, as text. */
.rt-used-sm { display: none; margin-top: 4px; }
@media (max-width: 640px) {
  .tbl .rt-col-used { display: none; }
  .rt-used-sm { display: block; }
}
.dbs-tz-change { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px; margin: 14px 0 6px; }
.dbs-tz-change .field-inline { display: flex; flex-direction: column; gap: 4px; flex: 1 1 260px; max-width: 420px; }
.dbs-tz-change select { width: 100%; }
.tbl th.num, .tbl td.num { text-align: right; white-space: nowrap; }

/* A TABLE CELL KEEPS ITS TABLE-CELL DISPLAY. `.host-actions` is a flex row, and
   on a <td> that took the cell out of its row: Server ▸ PHP's extension list drew
   its actions column on a grid of its own, each button a row away from the
   extension it acts on (seen on the beta box, 2 Oct 2026). The buttons still
   wrap; they are just spaced by margin instead of gap. */
td.host-actions { display: table-cell; text-align: end; white-space: normal; }
td.host-actions > * { margin: 2px 0 2px 6px; vertical-align: middle; }
/* "12 websites · 1 6amMart project" opens that version's websites */
.link-btn { background: none; border: 0; padding: 0; color: var(--accent, inherit); cursor: pointer; font: inherit; text-align: start; text-decoration: underline dotted; text-underline-offset: 3px; }
.link-btn:hover { text-decoration-style: solid; }

/* 1.5.2 §11.1 — an addon included with a box that ran SixPanel before addons, and the
   one-time "add 6amMart" steps */
.addon-included { margin: 8px 0 0; padding: 10px 12px; border-radius: 10px; background: var(--ok-soft); }
.addon-included .hint { margin: 2px 0 0; }
.addon-how { border-inline-start: 3px solid var(--accent); }
.addon-steps { margin: 8px 0 0; padding-inline-start: 20px; display: grid; gap: 6px; }
.addon-steps li { line-height: 1.5; }

/* 1.5.2 §13.4 — the Terminal (js/pages/terminal.js). The shell keeps the job log's
   dark console look in BOTH themes (--console-*), sized to most of the screen
   with a floor a phone can still type into; xterm.js fits its columns to it. */
.term-head { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 10px 16px; }
.term-head-text { flex: 1 1 320px; min-width: 0; }
.term-head-text h3 { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.term-host { margin: 12px 0 6px; height: 62vh; min-height: 320px; max-height: 900px; padding: 8px 6px 8px 10px;
  background: var(--console-bg); border-radius: var(--r-ctl); overflow: hidden; }
.term-host .xterm { height: 100%; }
.term-fp { font-size: 11.5px; word-break: break-all; }
.term-add { margin-top: 14px; }
.term-add textarea.input { width: 100%; min-height: 64px; font-size: 12px; }
/* the guide a coding agent is given (Terminal → Coding agents): read here as it is written there */
.term-guide { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 420px; overflow: auto; font-size: 12px; margin-top: 10px; }
@media (max-width: 560px) {
  .term-host { height: 55vh; min-height: 260px; padding: 6px 4px 6px 6px; }
}

/* AI Coding (js/pages/aicoding.js, docs/ROUND-AI-CODING.md §4.1). The server's page is a
   column of cards. A project's page is sessions | the conversation | where it runs, at the
   height of the window, each scrolling on its own — and one of the three at a time, by tab,
   where there is no room for three. The conversation itself is drawn with the Build with AI
   page's classes (.aib-*, .ai-msg, .ai-composer), so the two read as one product. */
.acd { display: flex; flex-direction: column; gap: 12px; }
.acd .card { margin-bottom: 0; }
.acd-top { padding: 10px 14px; }
.acd-what { margin: 0; font-size: 13.5px; }
.acd-top .hint { margin: 6px 0 0; }
.acd-stop { color: var(--err-fg); font-weight: 600; font-size: 13.5px; margin: 0 0 10px; }
.acd-warn { color: var(--warn-fg); }
.acd-state { margin: 0 0 10px; font-size: 13.5px; }
.acd-first { display: flex; flex-direction: column; gap: 12px; }
.acd-first:empty { display: none; }
.acd-firstcard { border-inline-start: 3px solid var(--warn); }
.acd-field { margin: 10px 0; }
.acd-field label { display: block; font-size: 12.5px; color: var(--muted); margin-bottom: 4px; }
.acd-field select, .acd-field .input { width: 100%; }
.acd-field .hint { margin: 4px 0 0; }
.acd-req { display: grid; gap: 2px; }
.acd-req-row { display: grid; grid-template-columns: minmax(110px, 1.1fr) minmax(0, 1fr) minmax(0, 1.6fr) auto; align-items: center; gap: 6px 12px; padding: 7px 0; border-bottom: 1px solid var(--border); font-size: 13px; }
.acd-req-row:last-child { border-bottom: 0; }
.acd-req-row .hint { margin: 0; }
.acd-engines { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); gap: 14px; margin-top: 12px; }
.acd-engine { border: 1px solid var(--border); border-radius: 12px; padding: 12px 14px; min-width: 0; }
.acd-eng-name { margin: 0 0 8px; font-size: 14px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.acd-engine .btn-row { flex-wrap: wrap; }
.acd-checks { margin: 4px 0 0; padding-inline-start: 18px; font-size: 13px; }
.acd-held { margin-top: 12px; }
.acd-held p { margin: 0 0 6px; }
.acd-held-rep { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.acd-rows { display: flex; flex-direction: column; }
.acd-row { display: flex; align-items: center; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--border); min-width: 0; }
.acd-row:last-child { border-bottom: 0; }
.acd-row-main { flex: 1 1 0; min-width: 0; overflow-wrap: anywhere; }
.acd-row-main .hint { margin: 2px 0 0; }
.acd-row-link { color: inherit; text-decoration: none; border-radius: 8px; }
.acd-row-link:hover, .acd-row-link:focus-visible { background: color-mix(in srgb, var(--muted) 8%, transparent); outline: none; }
.acd-row-chips { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }

.seg.acd-tabs { display: none; align-self: flex-start; }
.acd-main { display: grid; grid-template-columns: minmax(200px, 250px) minmax(0, 1fr) minmax(250px, 330px); gap: 16px; height: calc(100dvh - 300px); min-height: 520px; }
.acd-side, .acd-right { display: flex; flex-direction: column; padding: 14px; min-height: 0; min-width: 0; overflow-y: auto; overscroll-behavior: contain; }
.acd-side-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
.acd-side-head h3, .acd-right h3 { margin: 0; }
.acd-seslist { display: flex; flex-direction: column; gap: 4px; }
.acd-ses { display: flex; align-items: center; gap: 2px; border-radius: 10px; min-width: 0; }
.acd-ses.on { background: var(--accent-soft); }
.acd-ses-open { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 4px; padding: 7px 8px; border: 0; border-radius: 10px; background: none; color: inherit; font: inherit; text-align: start; cursor: pointer; }
.acd-ses-open:hover, .acd-ses-open:focus-visible { background: color-mix(in srgb, var(--muted) 10%, transparent); outline: none; }
.acd-ses-open b { font-size: 13.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acd-ses-sub { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 12px; }
.acd-ses-sub .hint { margin: 0; font-size: 12px; }
.btn.acd-ic { min-width: 30px; padding: 0; }
/* The conversation is a column of fixed height: its head, the composer and what waits for
   the owner keep their own height, and the thread takes what is left — never less than a
   few lines, so a long question never hides what it is about. */
.acd-chat { min-height: 0; }
.acd-chat .aib-thread { min-height: 80px; }
.acd-chat .ai-composer { flex: none; }
.acd-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-height: 26px; flex: none; }
.acd-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; }
.acd-usage { margin: 0 0 0 auto; margin-inline: auto 0; font-size: 12px; }
.acd-wait { flex: 0 1 auto; min-height: 0; display: flex; flex-direction: column; }
.acd-wait:empty { display: none; }
.acd-wait .card { margin-bottom: 0; }
.acd-ask { border: 1px solid color-mix(in srgb, var(--warn) 45%, var(--border)); padding: 12px 14px; max-height: 46vh; min-height: 0; overflow-y: auto; box-shadow: none; }
.acd-ask-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; font-size: 13.5px; }
.acd-ask-what { margin: 0 0 6px; font-size: 13.5px; overflow-wrap: anywhere; }
.acd-ask code { direction: ltr; unicode-bidi: isolate; font-family: var(--mono); font-size: 12.5px; overflow-wrap: anywhere; }
.acd-ask .aib-diff { max-height: 180px; }
.acd-q { border: 0; padding: 0; margin: 8px 0; min-width: 0; }
.acd-q legend { padding: 0; font-weight: 600; font-size: 13.5px; margin-bottom: 4px; }
.acd-q .check { align-items: flex-start; }
.acd-q .check input { margin-top: 3px; flex: none; }
.acd-q .input { width: 100%; margin-top: 4px; }
.acd-cant { flex: 1 1 180px; margin: 0; color: var(--warn-fg); }
.acd-cant:empty { display: none; }
/* the two side panels fold away on a wide screen (round 158, as in the Claude Code desktop
   app); the conversation then takes the room, its words in one readable column */
.acd-tog { min-width: 32px; padding-inline: 8px; font-size: 15px; line-height: 1; }
.acd-tog[aria-pressed="true"] { color: var(--accent); }
.acd-head .acd-tog[data-tog="right"] { margin-inline-start: auto; }
.acd-head .acd-new + .acd-tog[data-tog="right"] { margin-inline-start: 0; }
@media (min-width: 1101px) {
  .acd-main[data-side-off] > .acd-side, .acd-main[data-right-off] > .acd-right { display: none; }
  .acd-main[data-side-off] { grid-template-columns: minmax(0, 1fr) minmax(250px, 330px); }
  .acd-main[data-right-off] { grid-template-columns: minmax(200px, 250px) minmax(0, 1fr); }
  .acd-main[data-side-off][data-right-off] { grid-template-columns: minmax(0, 1fr); }
  .acd-main[data-side-off][data-right-off] .aib-col,
  .acd-main[data-side-off][data-right-off] .ai-composer { width: 100%; max-width: 860px; margin-inline: auto; }
}
/* the composer's "how" chip (round 158): one line, never wider than the row it sits in */
.ai-composer .btn-row { flex-wrap: wrap; min-width: 0; }
.acd-how { max-width: 100%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-weight: 500; }
.acd-how[hidden] { display: none; }
@media (max-width: 640px) { .acd-how { max-width: 48vw; } }
.acd-cmds { margin: 0; }
.acd-think > summary { cursor: pointer; font-size: 12px; color: var(--muted); padding-inline-start: 6px; }
.acd-right .kv { flex-wrap: wrap; gap: 2px 10px; }
.acd-right .kv > span:last-child { min-width: 0; overflow-wrap: anywhere; }
.acd-right > .hint { margin: 4px 0 0; }
.acd-right > h3 + .hint, .acd-right > h3 + .acd-stop { margin-top: 8px; }
.acd-right > h3:not(:first-child) { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border); }
.acd-opt { margin-top: 12px; align-items: flex-start; }
.acd-opt input, .acd-agree input { margin-top: 3px; flex: none; }
.acd-agree { align-items: flex-start; margin-top: 8px; }
.acd-ready { margin-top: 10px; }
.acd-ready p { margin: 0 0 8px; }
.acd-foot { margin-top: auto; padding-top: 14px; }
.acd-foot .hint { margin: 0 0 8px; }
/* The conversation, round 156. A tool row opens to what a line cannot hold — the whole
   command, the plan, what came back: words, not a diff, so they wrap. On the row itself
   the count and the time keep their width and the path gives way — from its FRONT, so the
   file's name is what stays (the run inside is left-to-right on every page). */
.aib-said, .aib-out { white-space: pre-wrap; overflow-wrap: anywhere; }
.aib-out { max-height: 260px; }
.acd-chat .aib-meta { flex: none; }
.acd-chat .aib-tool code.acd-path { direction: rtl; }
.acd-chat .aib-tool code.acd-path > bdi { direction: ltr; unicode-bidi: isolate; }
.acd-chat details.aib-tool[open] > summary code { direction: ltr; white-space: pre-wrap; overflow-wrap: anywhere; }
/* the way back to the newest words: Tia's own button (.aic-jump), hung over the thread's lower edge */
.acd-jumpbox { position: relative; height: 0; flex: none; margin-top: -10px; }
/* a new session in one press where the list of sessions is on another tab (drawn below 1100px) */
.acd-new { display: none; margin-inline-start: auto; }
.acd-usage + .acd-new { margin-inline-start: 0; }
/* what can be pressed says where the keyboard is, and is a finger's 24 px */
.acd-row-link:focus-visible, .acd-ses-open:focus-visible, .acd-chat details.aib-tool > summary:focus-visible, .acd-think > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.acd-think > summary { padding-block: 3px; }
.acd-chat .aic-code-copy { min-width: 24px; min-height: 24px; }
#acdGit { margin-top: 12px; min-width: 0; }
#acdGit .tf-card h3 { margin: 0 0 6px; }
/* the git card in a column a third of the page wide — or a phone: a long path wraps */
#acdGit code, #acdGit a { overflow-wrap: anywhere; white-space: normal; }
/* No room for three columns: one at a time, by tab, and as tall as what it holds — the page
   scrolls, the thread keeps a height of its own, and a question is shown whole. */
@media (max-width: 1100px) {
  .seg.acd-tabs { display: inline-flex; }
  .acd-main { grid-template-columns: minmax(0, 1fr); height: auto; min-height: 0; }
  .acd-main[data-tab="sessions"] > :not(.acd-side), .acd-main[data-tab="chat"] > :not(.acd-chat), .acd-main[data-tab="where"] > :not(.acd-right) { display: none; }
  .acd-side, .acd-right { overflow: visible; }
  .acd-chat .aib-thread { flex: 0 1 auto; min-height: 160px; max-height: 58dvh; }
  .acd-wait { flex: none; }
  .acd-ask { max-height: none; }
  .acd-new { display: inline-flex; }
}
@media (max-width: 560px) {
  .acd-req-row { display: flex; flex-wrap: wrap; gap: 2px 10px; }
  .acd-req-row b { flex: 1 1 100%; }
  .acd-row { flex-wrap: wrap; }
  .acd-usage { margin-inline: 0; flex: 1 1 100%; order: 1; }
  .acd-usage + .acd-new { margin-inline-start: auto; }
}
