/* --------------------------------------------------------------------------
   screens.css — the stylesheet for every screen both products share.

   It moved here from apps/onprem/admin/assets/app.css when the SaaS console
   turned out to be rendering 59 of these classes with no styling at all: the
   screens were shared, the routes were shared, and the sheet that dresses them
   was not. Both products load this now.

   The palette below is DEFAULTS. Each product sets the same custom properties
   before this file loads and wins:

     on-premise  — from the operator's Branding page (brand.css)
     hosted      — from the template kit's tokens, so the console matches the
                   marketing site and the platform's own brand colour

   Deployed path is unchanged: deploy composes this into admin/assets/app.css,
   which is what the on-premise layout has always linked.
   -------------------------------------------------------------------------- */

/* ==========================================================================
   Inbox assistant — admin UI
   --------------------------------------------------------------------------
   Hand-written, no build step, no framework. Everything that looks
   interactive here is CSS only (:target, :focus-within, :checked) and every
   screen works with script switched off.

   There is one script — assets/app.js, mirrored from apps/console/console.js —
   and its whole job is to say that a button was pressed. The CSP is
   script-src 'self', which permits that one same-origin file and still
   forbids every inline script; inline is the only way an escaping slip in a
   mail body could run anything, so the protection that mattered is intact.

   Brand orange #FF6200 is sampled from the AirGo wordmark. It is used for
   identity and for the single primary action on a page — never for status.
   Status has its own colours, because "the thing you click" and "the thing
   that is wrong" must never look the same.
   ========================================================================== */

/* --- cross-page motion ---------------------------------------------------
   ⚠ Every screen here is a full page load - there is no client-side router
   to animate between routes, and this file's own opening comment is clear
   that a script is not allowed to be the only way anything works. The
   Cross-Document View Transitions API is the platform answering that
   exact shape of problem: a stylesheet declaration, no script, and the
   browser itself crossfades the old page into the new one during
   navigation. Unsupported browsers ignore the rule and get exactly what
   they have always had - a plain navigation, nothing broken.
   A reduced-motion request turns it off entirely rather than shortening it:
   the ask was for LESS motion, and a fast crossfade is still motion. */
@view-transition { navigation: auto; }
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

/* --- palette ------------------------------------------------------------ */
:root {
  --brand:        #FF6200;
  --brand-ink:    #C74B00;
  --brand-soft:   #FFF1E8;
  --brand-line:   #FFD3B8;

  --bg:           #F6F7F9;
  --surface:      #FFFFFF;
  --surface-2:    #FBFCFD;
  --line:         #E3E7EC;
  --line-strong:  #CBD2DA;

  --ink:          #12171D;
  --ink-2:        #4A5560;
  --ink-3:        #5E6874;   /* was #79838F — ~3.5:1 at 12px, under the AA floor */

  --good:         #0F7B4F;
  --good-soft:    #E7F6EE;
  --warn:         #9A6400;
  --warn-soft:    #FDF3DF;
  --bad:          #B3261E;
  --bad-soft:     #FDECEA;
  --info:         #1B5FA8;
  --info-soft:    #E9F2FB;

  --radius:       12px;
  --radius-sm:    8px;
  --shadow:       0 1px 2px rgba(16,24,32,.06), 0 4px 16px rgba(16,24,32,.05);
  --shadow-lift:  0 2px 6px rgba(16,24,32,.08), 0 12px 32px rgba(16,24,32,.10);

  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

/* ⚠ THERE IS DELIBERATELY NO DARK PALETTE HERE, and removing the one that
   was is a fix rather than a loss. This sheet defines 42 tokens; console.css
   defines its own 22 from the kit and has never had a dark variant for any of
   them. So a phone set to dark rendered a page where the twenty tokens named
   here went dark and everything else stayed white: black wizard steps and a
   black guide panel on a light grey page.

   Supporting dark properly means giving the kit tokens a dark story too, which
   is a design job rather than a colour swap. Until somebody does that, the
   honest thing is to be a light interface on every device, which is what the
   color-scheme meta in both layouts now says as well. */

/* --- base --------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.6 var(--sans);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { line-height: 1.25; margin: 0 0 .4em; letter-spacing: -.01em; font-weight: 650; }
h1 { font-size: 26px; }
h2 { font-size: 18px; margin-top: 2rem; }
h3 { font-size: 15px; }

/* ⚠ NO UNDERLINE ON HOVER, ANYWHERE IN EITHER PANEL. This was the one rule
   underneath every hover that did not draw its own: a card, a promo box, a
   plain row all fell through to it the moment they had no more specific
   :hover of their own, which is what put a line of underlined text through
   the "Claude, ChatGPT or Gemini can connect a mailbox for you" box in the
   sidebar - a whole clickable card, reading like three words of body text
   somebody had linked by hand.

   A very small lift instead: the same "stand out", just spent on the
   element's position rather than drawn under its words.
   ⚠ TRANSFORM ONLY, NO COLOUR CHANGE - a colour WAS here once and it broke
   every card that sets its text colour on the <a> and lets children inherit
   it (.mesh, .brand's dark-panel variant): the moment this rule's `color`
   out-specificities a component's own resting colour on :hover, every
   descendant with no colour of its own repaints in the plain link colour,
   which is exactly what turned the platform overview's "Answered this
   month" card orange under the pointer. A transform never has that failure
   mode - it cannot leak into a child the way an inherited property can -
   so it is the one property this rule is safe to set globally. */
a { color: var(--brand-ink); text-decoration: none; transition: transform .16s var(--ease, ease); }
a:hover { transform: translateY(-1px); }

p { margin: 0 0 1rem; }
code, pre, .mono { font-family: var(--mono); font-size: .92em; }

/* Visible focus everywhere. Without JS this is the only affordance a
   keyboard user gets, so it is deliberately loud. */
:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: 4px;
}

.muted    { color: var(--ink-3); }
.err-text { color: var(--bad); }

/* --- app shell ---------------------------------------------------------- */
.bar {
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 20;
}

.bar-inner {
  max-width: 1120px; margin: 0 auto;
  padding: 0 24px;
  display: flex; align-items: center; gap: 28px;
  min-height: 62px;
}

.brand { display: flex; align-items: center; gap: 12px; flex: none; }
.brand img { height: 26px; width: auto; display: block; }

.bar nav { display: flex; gap: 2px; flex: 1 1 auto; flex-wrap: wrap; }
.bar nav a {
  padding: 8px 14px; border-radius: var(--radius-sm);
  color: var(--ink-2); font-weight: 550; font-size: 14px;
  display: inline-flex; align-items: center; gap: 7px;
}
.bar nav a:hover { background: var(--surface-2); color: var(--ink); text-decoration: none; }
.bar nav a.on { background: var(--brand-soft); color: var(--brand-ink); }

/* The count of things waiting rides in the nav, so it is visible from every
   page rather than only the dashboard. */
.navcount {
  min-width: 20px; padding: 0 6px; height: 20px;
  border-radius: 10px; background: var(--bad); color: #fff;
  font-size: 11px; font-weight: 700; line-height: 20px; text-align: center;
}
.bar nav a.on .navcount { background: var(--bad); }

.bar-right { display: flex; align-items: center; gap: 10px; flex: none; }

main { max-width: 1120px; margin: 0 auto; padding: 28px 24px 72px; }

/* --- pills -------------------------------------------------------------- */
.pill {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 10px; border-radius: 999px;
  font-size: 12px; font-weight: 650; letter-spacing: .01em;
  border: 1px solid transparent; white-space: nowrap;
}
.pill::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; flex: none;
}
.pill-plain::before { display: none; }

.mode-observe  { background: var(--info-soft); color: var(--info); border-color: var(--info); }
.mode-assisted { background: var(--warn-soft); color: var(--warn); border-color: var(--warn); }
.mode-auto     { background: var(--good-soft); color: var(--good); border-color: var(--good); }
.paused        { background: var(--bad-soft);  color: var(--bad);  border-color: var(--bad); }

/* ⚠ The pause pill in the bar is a BUTTON now, not a caption — the switch it
   reports on moved out of Settings and into the chrome. A button carries the
   user agent's own font, background and border, so those have to be taken back
   or it renders as a grey system control wedged between two pills.

   The pill's ::before dot goes: the icon already says which state this is, and
   two marks on one small control read as a bullet nobody placed. */
.barpause button {
  font: inherit; cursor: pointer;
  background: var(--surface-2); color: var(--ink-2); border-color: var(--line-strong);
  transition: background .15s ease, color .15s ease, border-color .15s ease;
}
.barpause button::before { display: none; }
.barpause button svg { width: 13px; height: 13px; }
.barpause button:hover { color: var(--ink); border-color: var(--ink-3); }
/* Paused means mail is arriving and nothing is answering it. It keeps the
   alarm colour it had as a caption. */
.barpause.is-paused button { background: var(--bad-soft); color: var(--bad); border-color: var(--bad); }
.barpause.is-paused button:hover { background: var(--bad-soft); color: var(--bad); border-color: var(--bad); }

.st-queued     { background: var(--surface-2); color: var(--ink-2); border-color: var(--line-strong); }
.st-replied    { background: var(--good-soft); color: var(--good); border-color: var(--good); }
.st-escalated  { background: var(--warn-soft); color: var(--warn); border-color: var(--warn); }
.st-error      { background: var(--bad-soft);  color: var(--bad);  border-color: var(--bad); }
.st-dismissed  { background: var(--surface-2); color: var(--ink-3); border-color: var(--line); }
/* ⚠ The mailbox list marked a disabled address `st-ignored`, which is not one
   of the statuses and has never had a rule: the pill rendered untinted, so an
   address that is switched off looked the same as one that is on. A state that
   stops mail being answered has to be visible. */
.st-off        { background: var(--warn-soft); color: var(--warn); border-color: var(--warn); }
/* ⚠ The alarm colour, not the warning one, and it is the only status that gets
   it besides an outright error. A held message may be an impersonation of the
   business reading it; that has to out-shout every other pill on the screen. */
.st-held       { background: var(--bad-soft);  color: var(--bad);  border-color: var(--bad); }

/* ⚠ The hosted sheet has had this since buttons were built; this one never did,
   because no shared screen used `small` until the Suspicious list wanted it on
   its Withdraw button. Both products render apps/console/views, so a word only
   one sheet knows renders full-size on the other.

   ⚠ A literal size, not --text-tagline-3 as the hosted rule uses: that token
   comes from kit-tokens.css, which only the hosted console loads. Referencing
   it here would resolve to nothing on-premise and the button would inherit
   whatever it sat next to. */
.btn.small { height: 2.4rem; font-size: .82rem; padding: .15rem 1rem; }

/* --- sender provenance -------------------------------------------------- */
/* The verdict line above the facts. .warn existed only on a flash, and the
   panel needs the same colour without the banner around it. */
.warn { color: var(--bad); font-weight: 600; }

/* The delivery path, inside a <details>. Monospaced because these are hostnames
   and IP addresses read character by character - "acmetool" against "acmetools"
   is the whole question on this screen, and a proportional font hides it. */
ol.hops { margin: .5rem 0 0; padding-left: 1.4rem; }
ol.hops li {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .82rem;
  color: var(--ink-2);
  word-break: break-all;
  margin-bottom: .25rem;
}

/* --- status banner ------------------------------------------------------ */
/* The one thing a new operator must understand: is it sending or not. */
/* A block, not a flex row: this is a <details>, and its summary and body must
   stack. The flex row lives on the summary itself, further down. Left as flex
   here, the explanatory paragraph rendered as a narrow column beside the
   heading. */
.status {
  display: block;
  border-radius: var(--radius);
  border: 1px solid var(--line); background: var(--surface);
  box-shadow: var(--shadow); margin-bottom: 24px;
  overflow: hidden;
}
.status-icon {
  flex: none; width: 40px; height: 40px; border-radius: 10px;
  display: grid; place-items: center; font-size: 19px; font-weight: 700;
}
.status-title { font-weight: 650; font-size: 16px; margin: 0 0 2px; }
.status-body  { margin: 0; color: var(--ink-2); font-size: 14px; }
.status-body strong { color: var(--ink); }

.status-observe  { border-left: 4px solid var(--info); }
.status-observe  .status-icon { background: var(--info-soft); color: var(--info); }
.status-assisted { border-left: 4px solid var(--warn); }
.status-assisted .status-icon { background: var(--warn-soft); color: var(--warn); }
.status-auto     { border-left: 4px solid var(--good); }
.status-auto     .status-icon { background: var(--good-soft); color: var(--good); }
.status-paused   { border-left: 4px solid var(--bad); }
.status-paused   .status-icon { background: var(--bad-soft); color: var(--bad); }
/* Spacing the shared screens reach for. console.css has its own copy for the
   hosted panels; both sheets have to know every word the shared views use, or
   the rule renders on one product and silently does nothing on the other. */
.mt-sm { margin-top: .6rem; }

/* Not a mode — a fault. Used where the model did not answer at all, which must
   not be mistaken for a decision the assistant made. */
.status-error    { border-left: 4px solid var(--bad); }
.status-error    .status-icon { background: var(--bad-soft); color: var(--bad); }

/* --- stat cards --------------------------------------------------------- */
.cards {
  display: grid; gap: 14px; margin: 0 0 26px;
  grid-template-columns: repeat(auto-fit, minmax(158px, 1fr));
}
.card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 18px 18px 16px;
  display: flex; flex-direction: column; gap: 2px;
  box-shadow: var(--shadow); color: inherit;
}
a.card { transition: transform .12s ease, box-shadow .12s ease; }
a.card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); text-decoration: none; }

.card .n { font-size: 32px; font-weight: 680; line-height: 1.1; letter-spacing: -.02em; }
.card .l { font-size: 13px; color: var(--ink-3); font-weight: 550; }
.card .sub { font-size: 12px; color: var(--ink-3); margin-top: 6px; }

/* "Needs you" is the only card that is ever an action, so it is the only one
   that carries brand colour. */
.card.need { border-color: var(--brand-line); background: var(--brand-soft); }
.card.need .n { color: var(--brand-ink); }
.card.need .l { color: var(--brand-ink); font-weight: 650; }
.card.bad  { border-color: var(--bad); }
.card.bad .n { color: var(--bad); }

/* --- panels and tables -------------------------------------------------- */
.panel {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 18px 20px; margin: 0 0 20px;
  box-shadow: var(--shadow);
}
.panel-pause { border-left: 4px solid var(--warn); }

.tablewrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow); }

table { border-collapse: collapse; width: 100%; font-size: 14px; }
th, td { padding: 11px 14px; text-align: left; vertical-align: top; }
thead th {
  background: var(--surface-2); color: var(--ink-3);
  font-size: 11px; font-weight: 650; letter-spacing: .06em; text-transform: uppercase;
  border-bottom: 1px solid var(--line); white-space: nowrap;
}
tbody tr + tr td { border-top: 1px solid var(--line); }
tbody tr:hover td { background: var(--surface-2); }
td .subj { font-weight: 550; color: var(--ink); }
td .from { color: var(--ink-3); font-size: 13px; }
td.nowrap, th.nowrap { white-space: nowrap; }

.empty {
  text-align: center; padding: 44px 20px; color: var(--ink-3);
  background: var(--surface); border: 1px dashed var(--line-strong);
  border-radius: var(--radius);
}
.empty .big { font-size: 30px; display: block; margin-bottom: 8px; }

/* --- forms -------------------------------------------------------------- */
.field { margin: 0 0 20px; }
.field > label {
  display: block; font-weight: 600; font-size: 14px; margin-bottom: 6px; color: var(--ink);
}

input[type=text], input[type=password], input[type=email], input[type=number],
input[type=search], select, textarea {
  width: 100%; padding: 9px 12px;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  font: inherit; font-size: 14px;
}
textarea { resize: vertical; min-height: 96px; font-family: var(--mono); font-size: 13px; line-height: 1.55; }
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--brand);
  box-shadow: 0 0 0 3px rgba(255,98,0,.16);
}
input::placeholder, textarea::placeholder { color: var(--ink-3); }

/* --- the file input ------------------------------------------------------
   ⚠ Styled through ::file-selector-button, which is the only way to dress a
   file input without JavaScript. The trick everyone reaches for — hide the
   input, style a label, print the filename from script — cannot work here: the
   CSP allows one same-origin file and no inline script, and the filename would
   have to come from somewhere. So the native control stays and only its button
   is restyled; the browser keeps drawing "No file chosen", which is the part
   that actually reports the state.

   ⚠ The BOX is deliberately not redeclared. console.css already styles
   input[type=file] at the same specificity and loads second, so anything set
   here for padding, border or background would be a coin toss decided by file
   order — the exact failure that put icons on top of their own text. Only what
   that sheet does not touch is set. */
input[type=file] { cursor: pointer; }
input[type=file]::file-selector-button {
  margin-right: 12px; padding: 7px 14px;
  font: inherit; font-size: 13px; font-weight: 600; color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
input[type=file]:hover::file-selector-button {
  background: var(--brand-soft); border-color: var(--brand-line); color: var(--brand-ink);
}
input[type=file]:focus-visible::file-selector-button {
  outline: 2px solid var(--brand); outline-offset: 2px;
}

.help { font-size: 13px; color: var(--ink-3); margin: 8px 0 12px; line-height: 1.55; }

/* ⚠ pre-line, so a settings help string can carry a worked example - and ONLY
   there, on the two places that print one (settings.php, mailbox_wizard.php).
   It used to sit on .help itself, on the belief that help text never contains
   a newline. The strings in config.php do not, but every template paragraph
   and list wrapped across several source lines does: each of those newlines
   became a line break, so a bullet sat alone above its text, every item began
   and ended with an empty line, and sentences broke at wherever the PHP was
   wrapped. Runs of spaces still collapse, so an indented example line does not
   keep its indent - wrap those in <code> or a block of their own. */
.help-lines { white-space: pre-line; }

/* ── long field help, clamped ─────────────────────────────────────────────
   See the note above where this is built, in mailbox_wizard.php's
   $renderField(). A <details> whose <summary> IS the help text: clamped to
   two lines while closed, full height while open - one copy of the text,
   not a preview plus a separate full version to keep in sync. The toggle
   is a sibling span rather than ::after on the clamped text itself,
   because generated content inside a -webkit-line-clamp box is exactly as
   liable to be cut off as anything else in it - the first version of this
   put "Show more" there and it silently vanished the moment a paragraph's
   second line ran long enough to reach it. */
.help-more > summary { cursor: pointer; list-style: none; }
.help-more > summary::-webkit-details-marker { display: none; }
.help-more:not([open]) .help-more-text {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.help-more-toggle {
  display: inline-block; margin-top: 2px; font-size: 13px; font-weight: 500;
  color: var(--violet); cursor: pointer;
}
.help-more-toggle::after { content: "Show more"; }
.help-more[open] .help-more-toggle::after { content: "Show less"; }

.check { display: flex; align-items: flex-start; gap: 9px; font-size: 14px; margin: 0 0 6px; cursor: pointer; }
.check input { margin-top: 3px; accent-color: var(--brand); flex: none; }
.checks { display: grid; gap: 4px; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }

/* ── A threshold per category ─────────────────────────────────────
   Same shape as .checks so the two category fields read as one idea. The name
   takes the space it needs and the number stays a fixed width, because a column
   of numbers that do not line up is hard to compare - which is the entire reason
   somebody opens this. */
.catgrid { display: grid; gap: 4px 14px; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
.catrow {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  font-size: 14px; padding: 3px 0; cursor: pointer;
}
.catrow input {
  width: 6.5rem; flex: none; text-align: right;
  padding: 5px 8px; border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink); font-size: 14px;
}
.catrow input:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
/* A row given its own figure should be findable at a glance among ten that
   are not. :placeholder-shown is false exactly when the box has a value. */
.catrow input:not(:placeholder-shown) { border-color: var(--brand); font-weight: 600; }

/* ── Managing a tenant's own categories ──────────────────────────── */
.catmanage-item {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 14px;
}
.catmanage-item:last-of-type { border-bottom: 0; }
.catmanage-add { display: flex; gap: 10px; align-items: center; margin-top: 12px; flex-wrap: wrap; }
.catmanage-add .input-icon { flex: 1 1 240px; }

/* ⚠ Now nested inside the .setsec it extends, not a standalone .panel - it
   used to sit at the tab's foot as its own card, and this rule is what makes
   it read as a continuation of the checklist above rather than a second box
   dropped underneath it. */
.catmanage { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line); }
.catmanage-head {
  margin: 0 0 8px; font-size: 13px; font-weight: 700;
  display: flex; align-items: center; gap: 8px; color: var(--ink-2);
}
.catmanage-head .ico { width: 15px; height: 15px; color: var(--ink-3); }

/* ── a confidence figure, read as a percentage rather than a decimal ────────
   0.97 is what the code compares a model's own output against; 97% is what
   somebody who has never seen the model's output types with confidence. */
.pct-icon input[type] { padding-right: 34px; }
.pct-suffix {
  position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
  color: var(--ink-3); font-size: 14px; font-weight: 600; pointer-events: none;
}
.catrow-input { position: relative; display: inline-flex; flex: none; }
.catrow-input input {
  padding-right: 22px;   /* room for the % without widening the column */
}
.catrow-suffix {
  position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
  color: var(--ink-3); font-size: 12px; font-weight: 600; pointer-events: none;
}

.group-heading {
  margin: 34px 0 14px; padding-bottom: 8px;
  border-bottom: 2px solid var(--line);
  font-size: 15px; font-weight: 650;
  color: var(--ink);
}

/* ⚠ THE BUTTON BELONGS ON THE LIST THAT HAS ROWS, not bolted underneath it
   whether or not there are any. A list with nothing in it already carries
   its own "create one" through empty_state()'s own action - this is only
   for once a list is non-empty and that empty state is gone, so "add
   another" lives beside what it is adding to instead of at the bottom of
   the page, past everything already there. .group-heading keeps its own
   border and spacing; this only turns it into a row when a button joins it. */
.list-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin: 34px 0 14px; padding-bottom: 8px; border-bottom: 2px solid var(--line);
}
.list-head .group-heading { margin: 0; padding: 0; border: 0; flex: 1 1 auto; }

/* --- buttons ------------------------------------------------------------ */
button, .btn {
  font: inherit; font-size: 14px; font-weight: 600;
  padding: 9px 18px; border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong); background: var(--surface); color: var(--ink);
  cursor: pointer; display: inline-flex; align-items: center; gap: 7px;
  transition: background .12s ease, border-color .12s ease;
}
button:hover, .btn:hover { background: var(--surface-2); text-decoration: none; }

/* ⚠ Only console.css (the hosted product) overrides this to the near-black
   pill+capsule treatment - this base rule is what the on-premise admin
   actually ships, since it loads app.css (mirrored from THIS file) alone.
   Motion, not a reshape: a lift and a brand-coloured glow on hover give it
   the same "reaching for it" feeling console.css's own primary button has,
   without changing its shape here, which is out of scope for this pass. */
button.primary, .btn.primary {
  background: var(--brand); border-color: var(--brand); color: #fff;
  box-shadow: 0 1px 2px rgba(199,75,0,.28);
  transition: background .12s ease, border-color .12s ease,
              transform .22s ease, box-shadow .22s ease;
}
button.primary:hover, .btn.primary:hover {
  background: var(--brand-ink); border-color: var(--brand-ink);
  transform: translateY(-2px);
  box-shadow: 0 10px 22px -10px color-mix(in srgb, var(--brand) 60%, transparent);
}
button.primary:active, .btn.primary:active {
  transform: translateY(0);
  box-shadow: 0 3px 8px -4px color-mix(in srgb, var(--brand) 55%, transparent);
  transition-duration: .06s;
}
@media (prefers-reduced-motion: reduce) {
  button.primary, .btn.primary { transition: background .12s ease, border-color .12s ease; }
  button.primary:hover, .btn.primary:hover, button.primary:active, .btn.primary:active { transform: none; }
}

button.danger { color: var(--bad); border-color: var(--bad); background: transparent; }
button.danger:hover { background: var(--bad-soft); }

/* ── the page-load bar, and busy controls ─────────────────────────────────
   ⚠ THIS EXISTED, JUST NOT HERE. console.js has driven a .pageload bar and
   an .is-busy spinner state on every submit and navigation since they were
   built, and console.css (the hosted product's own stylesheet) has carried
   the CSS for both since then. The on-premise admin runs the same script -
   app.js is mirrored from console.js - and loads THIS file, not
   console.css, so every one of those classList.add() calls has been
   landing on an element with no rule for it: no bar, no spinner, nothing
   visible, on every page load and every button press, for as long as
   console.js has existed. Ported rather than duplicated by drifting: same
   shape, this file's own --brand/--ink tokens in place of console.css's
   --violet. */
/* ⚠ Richer, not more dishonest - it still never claims to know how long the
   request takes: eases toward nine tenths and waits, same as always. The
   gradient, the glow and the pulse are presence, not a fake percentage. */
.pageload {
  position: fixed; top: 0; left: 0; height: 3px; width: 100%; transform-origin: 0 50%;
  transform: scaleX(0);
  background: linear-gradient(90deg, color-mix(in srgb, var(--brand) 55%, #fff) 0%, var(--brand) 100%);
  box-shadow: 0 0 10px -1px color-mix(in srgb, var(--brand) 75%, transparent);
  z-index: 200; pointer-events: none;
}
.pageload-on {
  transform: scaleX(.9); transition: transform 8s cubic-bezier(.05,.7,.1,1);
  animation: pageload-pulse 1.4s ease-in-out infinite;
}
.pageload-done { transform: scaleX(1); opacity: 0; animation: none;
                 transition: transform .2s linear, opacity .35s .15s linear; }
@keyframes pageload-pulse {
  0%, 100% { box-shadow: 0 0 10px -1px color-mix(in srgb, var(--brand) 75%, transparent); }
  50%      { box-shadow: 0 0 18px 1px color-mix(in srgb, var(--brand) 95%, transparent); }
}

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

/* The button keeps its label and its width - a spinner that replaces the
   text makes the button jump, and a control that changes size under the
   cursor is worse than one that says nothing. The label dims, the ring
   turns beside it. */
.btn.is-busy, button.is-busy { pointer-events: none; position: relative; }
.btn.is-busy::after, button.is-busy::after {
  content: ""; width: 1rem; height: 1rem; margin-left: .55rem; flex: none;
  border-radius: 50%; border: 2px solid currentColor; border-top-color: transparent;
  opacity: .85; animation: spin .7s linear infinite;
}
.btn.is-busy { opacity: .82; }

/* A link is text, not a box, so it gets the ring on its own rather than a
   whole button's worth of state. */
a.is-busy { position: relative; opacity: .6; }
.side-nav a.is-busy::after, .switcher .menu a.is-busy::after {
  content: ""; width: .85rem; height: .85rem; margin-left: auto; flex: none;
  border-radius: 50%; border: 2px solid currentColor; border-top-color: transparent;
  animation: spin .7s linear infinite;
}
/* A busy nav row has its count replaced by the spinner; two circles fighting
   for the same corner reads as a glitch. */
.side-nav a.is-busy .tag { display: none; }

@media (prefers-reduced-motion: reduce) {
  .btn.is-busy::after, button.is-busy::after,
  .side-nav a.is-busy::after, .switcher .menu a.is-busy::after { animation: none; opacity: .5; }
  .pageload-on { transition: none; transform: scaleX(.9); animation: none; }
}

button.link {
  background: none; border: none; padding: 4px 8px;
  color: var(--ink-2); font-weight: 550; text-decoration: none;
}
button.link:hover { color: var(--ink); background: var(--surface-2); }

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

/* --- flash -------------------------------------------------------------- */
.flash {
  padding: 12px 16px; border-radius: var(--radius-sm); margin: 0 0 16px;
  background: var(--info-soft); color: var(--info);
  border: 1px solid currentColor; font-size: 14px;
}
.flash a { color: inherit; text-decoration: underline; font-weight: 600; }
.flash-err { background: var(--bad-soft); color: var(--bad); }
.flash-ok  { background: var(--good-soft); color: var(--good); }

/* ⚠ The shared screens spell these the hosted console's way — `flash good`,
   `flash warn`, `flash bad` — because that is the vocabulary they were written
   in. On-premise only ever defined flash-ok and flash-err, so a warning on a
   shared screen rendered in the neutral tint here and nowhere said so. Same
   colours, both spellings, one sheet. */
.flash.good { background: var(--good-soft); color: var(--good); }
.flash.warn { background: var(--warn-soft); color: var(--warn); }
.flash.bad  { background: var(--bad-soft);  color: var(--bad); }

/* ⚠ Every banner in all three panels carries its tone in colour alone. Colour
   alone is the weakest signal there is: it is the first thing lost to a
   colour-blind reader, a dim screen or a glance. The icon says the same thing
   a second way, and it is done here rather than in thirty pieces of markup
   because there are thirty places that render a flash and a rule reaches all
   of them at once.

   Masked rather than drawn, so the glyph takes the banner's own colour and one
   sheet does not have to carry a copy of every icon in every tint. The data
   URI is an image as far as the policy is concerned, and img-src allows data:
   in both panels. */
/* ⚠ NOT display:flex, and that is the whole fix.

   Flex made the ::before icon item one and the message items two onward — but
   the message is not wrapped in anything, so every inline child became its own
   flex item and got its own column. A banner reading "<strong>No business
   information</strong> …prose… <a>Add it now</a> …prose…" came out as four
   columns side by side, with the link squeezed to one word per line.
   Plain-text flashes were unaffected, which is why it survived: they have
   exactly one child.
   
   Positioning the icon in a padded gutter leaves the message in normal inline
   flow, so it wraps as prose. Same look, and it still reaches all thirty
   call sites from one rule without any of them wrapping their content. */
.flash { position: relative; padding-left: 44px; }
.flash::before {
  content: ""; position: absolute; left: 16px; top: 13px;
  width: 18px; height: 18px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Ccircle%20cx=%2212%22%20cy=%2212%22%20r=%229%22/%3E%3Cpath%20d=%22M12%2011v5%22/%3E%3Ccircle%20cx=%2212%22%20cy=%227.6%22%20r=%22.6%22%20fill=%22%23000%22%20stroke=%22none%22/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Ccircle%20cx=%2212%22%20cy=%2212%22%20r=%229%22/%3E%3Cpath%20d=%22M12%2011v5%22/%3E%3Ccircle%20cx=%2212%22%20cy=%227.6%22%20r=%22.6%22%20fill=%22%23000%22%20stroke=%22none%22/%3E%3C/svg%3E") center / contain no-repeat;
}
.flash.good::before, .flash-ok::before {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Ccircle%20cx=%2212%22%20cy=%2212%22%20r=%229%22/%3E%3Cpath%20d=%22M8%2012.4l2.7%202.7L16%209.6%22/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Ccircle%20cx=%2212%22%20cy=%2212%22%20r=%229%22/%3E%3Cpath%20d=%22M8%2012.4l2.7%202.7L16%209.6%22/%3E%3C/svg%3E");
}
.flash.warn::before {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20d=%22M10.3%204.2%202.4%2018.3A1.9%201.9%200%200%200%204%2021.2h16a1.9%201.9%200%200%200%201.6-2.9L13.7%204.2a1.9%201.9%200%200%200-3.4%200Z%22/%3E%3Cpath%20d=%22M12%209.5v4.2%22/%3E%3Ccircle%20cx=%2212%22%20cy=%2217.2%22%20r=%22.6%22%20fill=%22%23000%22%20stroke=%22none%22/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20d=%22M10.3%204.2%202.4%2018.3A1.9%201.9%200%200%200%204%2021.2h16a1.9%201.9%200%200%200%201.6-2.9L13.7%204.2a1.9%201.9%200%200%200-3.4%200Z%22/%3E%3Cpath%20d=%22M12%209.5v4.2%22/%3E%3Ccircle%20cx=%2212%22%20cy=%2217.2%22%20r=%22.6%22%20fill=%22%23000%22%20stroke=%22none%22/%3E%3C/svg%3E");
}
.flash.bad::before, .flash-err::before {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20d=%22M10.3%204.2%202.4%2018.3A1.9%201.9%200%200%200%204%2021.2h16a1.9%201.9%200%200%200%201.6-2.9L13.7%204.2a1.9%201.9%200%200%200-3.4%200Z%22/%3E%3Cpath%20d=%22M12%209.5v4.2%22/%3E%3Ccircle%20cx=%2212%22%20cy=%2217.2%22%20r=%22.6%22%20fill=%22%23000%22%20stroke=%22none%22/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20d=%22M10.3%204.2%202.4%2018.3A1.9%201.9%200%200%200%204%2021.2h16a1.9%201.9%200%200%200%201.6-2.9L13.7%204.2a1.9%201.9%200%200%200-3.4%200Z%22/%3E%3Cpath%20d=%22M12%209.5v4.2%22/%3E%3Ccircle%20cx=%2212%22%20cy=%2217.2%22%20r=%22.6%22%20fill=%22%23000%22%20stroke=%22none%22/%3E%3C/svg%3E");
}

/* --- toasts: the same .flash, floated and self-dismissing ----------------
   ⚠ toast_stack() (apps/console/helpers.php) renders these in place of
   walking take_flash() directly - one call, both panels that queue a flash
   across a redirect (the hosted client console and the on-premise admin;
   the hosted PLATFORM panel never queues one, it renders its own results
   inline on the same request, which is a different thing and stays a
   banner). Every .flash colour and icon rule above already applies; this
   only adds the floating position and the two ways out. */
.toast-stack {
  position: fixed; top: 1rem; left: 50%; transform: translateX(-50%); z-index: 80;
  display: flex; flex-direction: column; gap: .5rem;
  width: min(30rem, calc(100vw - 2rem));
  /* The stack itself is not a target - only a toast inside it is - so
     nothing under it, in the empty gaps between toasts, is blocked. */
  pointer-events: none;
}
.toast-stack .flash {
  margin: 0; pointer-events: auto; box-shadow: var(--shadow, 0 12px 32px -18px rgba(18,22,31,.35));
  padding-right: 40px;
  /* Fades in at once; fades out on its own after a while, unless the two
     ways out below - a hover, or the close button - get there first. */
  animation: toast-in .2s ease both, toast-out .25s ease 7s forwards;
}
.toast-stack .flash:hover { animation-play-state: paused; }
/* ⚠ THE CHECKBOX IS INSIDE THE TOAST IT DISMISSES, not beside it the way
   the CSS-only modal's toggle sits beside what it reveals (.modal-toggle,
   below) - a toast needs no separate element for :has() to reach, since
   "checked" and "gone" both describe the one box. */
.toast-stack .flash:has(.toast-toggle:checked) { display: none; }
.toast-toggle {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
.toast-msg { display: block; }
.toast-close {
  position: absolute; top: 10px; right: 12px; cursor: pointer;
  display: flex; opacity: .55; transition: opacity .16s ease;
}
.toast-close:hover { opacity: 1; }
.toast-close .ico { width: 14px; height: 14px; }
.toast-toggle:focus-visible ~ .toast-close { outline: 2px solid currentColor; outline-offset: 2px; opacity: 1; }
@keyframes toast-in { from { opacity: 0; transform: translateY(-8px); } }
@keyframes toast-out { to { opacity: 0; transform: translateY(-6px); } }
/* Reduced motion loses the timed exit along with the entrance - the toast
   simply stays until the close button is pressed, rather than vanishing on
   a clock with no visible countdown for a reader who asked to see less
   motion, not less information. */
@media (prefers-reduced-motion: reduce) {
  .toast-stack .flash { animation: none; }
}
/* ⚠ A CORE CHANGE, NOT A NARROWER VERSION OF THE SAME BOX. Fixed-and-
   floating was the whole problem on a phone: the stack sits OUTSIDE normal
   flow at a fixed distance from the top of the VIEWPORT, so a toast long
   enough to need three or four lines - "Now in Answering mode. Assisted,
   except that..." is a real one this product sends - grew straight down
   over the header instead of pushing anything out of its way, covering the
   workspace switcher and the drawer button along with it. Widening it
   (the previous fix) made the box wider; it never touched why a tall one
   covered controls under it.
   static, not just "not fixed": toast_stack() renders at the very top of
   <main> (apps/saas/app/views/layout.php), right after the header in the
   DOM, so in normal flow it lands exactly where it visually needs to be -
   below the header, above the page - and pushes real content down instead
   of floating over any of it. Same breakpoint as the header's own collapse
   into the compact mobile bar (console.css, "@media (max-width: 1000px)"
   where .side becomes the grid top/foot/btn/nav strip), since that is the
   exact width range where a fixed toast has controls under it worth
   covering in the first place. */
@media (max-width: 1000px) {
  .toast-stack {
    position: static; width: auto; max-width: none;
    left: auto; right: auto; top: auto; transform: none; z-index: auto;
    margin: 0 0 1rem;
  }
  .toast-stack .flash { box-shadow: var(--shadow, 0 4px 16px rgba(16,24,32,.08)); }
}

/* And the same for help text under a field: the shared screens say .hint where
   on-premise says .help. Unstyled, it rendered at body size and read as
   instruction rather than as an aside. */
.hint { font-size: 13px; color: var(--ink-3); margin: 8px 0 12px; line-height: 1.55; }

/* --- message detail ----------------------------------------------------- */
.meta { display: grid; gap: 10px 26px; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); margin: 0 0 18px; }
.meta div { min-width: 0; }
.meta dt { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-3); font-weight: 650; }
.meta dd { margin: 2px 0 0; font-size: 14px; word-break: break-word; }

.emailbody {
  white-space: pre-wrap; word-wrap: break-word;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: 15px 17px;
  font-family: var(--mono); font-size: 13px; line-height: 1.6;
  max-height: 460px; overflow: auto;
}

/* A confidence number means nothing to a non-technical reader on its own, so
   it is always drawn as a bar against the threshold that governs it. */
.conf { display: flex; align-items: center; gap: 10px; }
.conf-track { flex: 1 1 auto; height: 7px; border-radius: 4px; background: var(--line); position: relative; overflow: hidden; max-width: 220px; }
.conf-fill { position: absolute; inset: 0 auto 0 0; border-radius: 4px; background: var(--good); }
.conf-fill.low { background: var(--warn); }
.conf-num { font-variant-numeric: tabular-nums; font-weight: 650; font-size: 13px; }

.logbox {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 14px 16px; font-family: var(--mono); font-size: 12.5px; line-height: 1.65;
  max-height: 560px; overflow: auto; white-space: pre-wrap; word-break: break-word;
}

/* --- login -------------------------------------------------------------- */
.login-wrap { min-height: 100vh; display: grid; place-items: center; padding: 24px; }
.login-card {
  width: 100%; max-width: 372px; background: var(--surface);
  border: 1px solid var(--line); border-radius: 16px; padding: 34px 30px;
  box-shadow: var(--shadow-lift);
}
.login-card .brand { justify-content: center; margin-bottom: 6px; }
.login-card .brand img { height: 30px; }
.login-card h1 { font-size: 17px; text-align: center; margin-bottom: 22px; color: var(--ink-2); font-weight: 550; }
.login-card button { width: 100%; justify-content: center; margin-top: 4px; }
.login-foot { text-align: center; margin-top: 18px; font-size: 12px; color: var(--ink-3); }

@media print {
  .bar, .actions, form { display: none; }
  body { background: #fff; }
}

/* Confidence bar widths. Classes rather than inline styles because the CSP
   is style-src 'self' - an inline style attribute would be blocked. */
.conf-fill.w0 { width: 0%; }
.conf-fill.w5 { width: 5%; }
.conf-fill.w10 { width: 10%; }
.conf-fill.w15 { width: 15%; }
.conf-fill.w20 { width: 20%; }
.conf-fill.w25 { width: 25%; }
.conf-fill.w30 { width: 30%; }
.conf-fill.w35 { width: 35%; }
.conf-fill.w40 { width: 40%; }
.conf-fill.w45 { width: 45%; }
.conf-fill.w50 { width: 50%; }
.conf-fill.w55 { width: 55%; }
.conf-fill.w60 { width: 60%; }
.conf-fill.w65 { width: 65%; }
.conf-fill.w70 { width: 70%; }
.conf-fill.w75 { width: 75%; }
.conf-fill.w80 { width: 80%; }
.conf-fill.w85 { width: 85%; }
.conf-fill.w90 { width: 90%; }
.conf-fill.w95 { width: 95%; }
.conf-fill.w100 { width: 100%; }

/* --- branding controls -------------------------------------------------- */
/* --- the colour picker ---------------------------------------------------
   ⚠ The native radio is gone, and the DOT is the control now.

   An unchecked radio is drawn by the browser as a grey ring. Nothing in this
   sheet put it there — appearance was `auto` and the widget was 13px of the
   browser's own chrome — so it could not be styled away, only replaced. Beside
   a row of saturated colour circles it read as a smudge.

   The input is still there, still focusable, still the thing a screen reader
   and the form both see; it is simply not painted. Selection is a ring around
   the dot, which is the same information in the place the eye is already
   looking. */
.swatch { align-items: center; position: relative; }
.check.swatch > input[type=radio] {
  position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; pointer-events: none;
}
.swatch-dot { width: 16px; height: 16px; border-radius: 50%; display: inline-block;
  border: 1px solid rgba(0,0,0,.15); flex: none;
  box-shadow: 0 0 0 0 transparent;
  transition: box-shadow .15s ease, transform .15s ease; }
.check.swatch:hover .swatch-dot { transform: scale(1.12); }
.check.swatch > input:checked ~ .swatch-dot {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 32%, transparent);
}
/* ⚠ Keyboard users lose the browser's own focus ring with the widget, so it is
   drawn back onto the dot. Without this the picker is unusable by keyboard —
   you can Tab through it and never see where you are. */
.check.swatch > input:focus-visible ~ .swatch-dot {
  outline: 2px solid var(--brand); outline-offset: 3px;
}
.check.swatch > input:checked ~ * { font-weight: 600; }

/* ⚠ The custom field is revealed by the Custom preset being chosen, and the
   rule HIDES rather than shows. That is deliberate: :has() is what makes this
   possible without JavaScript, and where it is unsupported the whole rule is
   dropped — leaving the field visible, which is exactly how it behaved before.
   A `display:block` on match would instead leave it permanently hidden there. */
.field:has(.checks input:checked:not([value="custom"])) + .custom-colour { display: none; }
.sw-orange   { background: #FF6200; }
.sw-blue     { background: #2563EB; }
.sw-teal     { background: #0D9488; }
.sw-green    { background: #16A34A; }
.sw-purple   { background: #7C3AED; }
.sw-red      { background: #DC2626; }
.sw-slate    { background: #475569; }
.sw-custom   { background: #FF6200; }
.asset-preview { display: flex; align-items: center; gap: 16px; margin: 0 0 8px;
  background: var(--surface-2); }
.asset-logo { max-height: 42px; max-width: 260px; width: auto; display: block; }
.asset-favicon { width: 32px; height: 32px; object-fit: contain; display: block; }
input[type=file] { width: 100%; padding: 9px 12px; font: inherit; font-size: 14px;
  background: var(--surface); color: var(--ink);
  border: 1px dashed var(--line-strong); border-radius: var(--radius-sm); }

/* A dark neutral, not var(--brand): on the hosted console that variable is the
   platform's colour, not this workspace's, so it misrepresented the email. The
   point of the band is only "this logo sits on a dark header, so a white mark
   reads best" — the exact hue is set elsewhere. */
.asset-preview.on-brand { background: var(--ink); border-color: var(--ink); }
.on-brand-text { color: #fff; opacity: .85; font-size: 13px; }

/* --- icons --------------------------------------------------------------- */
.ico { width: 18px; height: 18px; flex: none; vertical-align: -3px; }
.bar nav .ico { width: 17px; height: 17px; }
.check .ico, .help .ico { width: 15px; height: 15px; }

/* --- tabs ---------------------------------------------------------------- */
/* Radio inputs and sibling selectors. There is no JavaScript on this site. */
.tabradio { position: absolute; opacity: 0; pointer-events: none; }
/* ⚠ PILLS, NOT FILE-FOLDER TABS. This was the one strip in either panel
   still built as boxy, bottom-joined tabs with a baseline rule under them -
   every other row of screens-to-move-between here (.tabpills on the
   dashboard, .band on the mode scale, .side-nav itself) is a row of soft
   rounded pills, resting on var(--surface) and filling to var(--ink) when
   current. Same shape here now: only the .tab rule and the active-state
   declaration below change, never the :checked selectors that pick which
   one is active - that state machine has its own history of bugs already
   fixed once (see the false/null $__openSec note further down this file)
   and none of it needed to move to change how the strip looks. */
.tabs {
  display: flex; gap: 8px; flex-wrap: wrap;
  margin: 0 0 22px; padding-bottom: 0;
}
.tab {
  display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
  padding: 9px 16px; font-size: 14px; font-weight: 500; color: var(--ink-2);
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--surface); white-space: nowrap;
  transition: border-color .18s ease, color .18s ease, background .18s ease;
}
.tab:hover { color: var(--ink); border-color: var(--line-strong, var(--line)); }
.tabpanel { display: none; }

/* One rule per tab: highlight its label and reveal its panel. */
#tab-behaviour:checked ~ .tabs .tab[for="tab-behaviour"] { background: var(--ink); border-color: var(--ink); color: #fff; }
#tab-behaviour:checked ~ #panel-behaviour { display: block; }
#tab-incoming-mail:checked ~ .tabs .tab[for="tab-incoming-mail"] { background: var(--ink); border-color: var(--ink); color: #fff; }
#tab-incoming-mail:checked ~ #panel-incoming-mail { display: block; }
#tab-filing:checked ~ .tabs .tab[for="tab-filing"] { background: var(--ink); border-color: var(--ink); color: #fff; }
#tab-filing:checked ~ #panel-filing { display: block; }
#tab-filtering:checked ~ .tabs .tab[for="tab-filtering"] { background: var(--ink); border-color: var(--ink); color: #fff; }
#tab-filtering:checked ~ #panel-filtering { display: block; }
#tab-alert-email:checked ~ .tabs .tab[for="tab-alert-email"] { background: var(--ink); border-color: var(--ink); color: #fff; }
#tab-alert-email:checked ~ #panel-alert-email { display: block; }
#tab-api-keys:checked ~ .tabs .tab[for="tab-api-keys"] { background: var(--ink); border-color: var(--ink); color: #fff; }
#tab-api-keys:checked ~ #panel-api-keys { display: block; }
#tab-identity:checked ~ .tabs .tab[for="tab-identity"] { background: var(--ink); border-color: var(--ink); color: #fff; }
#tab-identity:checked ~ #panel-identity { display: block; }
#tab-outgoing-mail:checked ~ .tabs .tab[for="tab-outgoing-mail"] { background: var(--ink); border-color: var(--ink); color: #fff; }
#tab-outgoing-mail:checked ~ #panel-outgoing-mail { display: block; }
#tab-ai-connectors:checked ~ .tabs .tab[for="tab-ai-connectors"] { background: var(--ink); border-color: var(--ink); color: #fff; }
#tab-ai-connectors:checked ~ #panel-ai-connectors { display: block; }

/* Keyboard focus must be visible even though the radio itself is hidden. */
.tabradio:focus-visible ~ .tabs .tab { outline: none; }
.tabradio#tab-behaviour:focus-visible ~ .tabs .tab[for="tab-behaviour"] { outline: 2px solid var(--brand); outline-offset: 1px; }
.tabradio#tab-filtering:focus-visible ~ .tabs .tab[for="tab-filtering"] { outline: 2px solid var(--brand); outline-offset: 1px; }
.tabradio#tab-branding:focus-visible ~ .tabs .tab[for="tab-branding"] { outline: 2px solid var(--brand); outline-offset: 1px; }
.tabradio#tab-alert-email:focus-visible ~ .tabs .tab[for="tab-alert-email"] { outline: 2px solid var(--brand); outline-offset: 1px; }
.tabradio#tab-api-keys:focus-visible ~ .tabs .tab[for="tab-api-keys"] { outline: 2px solid var(--brand); outline-offset: 1px; }
.tabradio#tab-identity:focus-visible ~ .tabs .tab[for="tab-identity"] { outline: 2px solid var(--brand); outline-offset: 1px; }
.tabradio#tab-outgoing-mail:focus-visible ~ .tabs .tab[for="tab-outgoing-mail"] { outline: 2px solid var(--brand); outline-offset: 1px; }

/* Resting pills get weight too, not only the active one - a flat white pill
   on a flat white page was the whole complaint. */
.tab { box-shadow: 0 1px 2px rgba(16,24,32,.05); }
#tab-behaviour:checked ~ .tabs .tab[for="tab-behaviour"],
#tab-incoming-mail:checked ~ .tabs .tab[for="tab-incoming-mail"],
#tab-filing:checked ~ .tabs .tab[for="tab-filing"],
#tab-filtering:checked ~ .tabs .tab[for="tab-filtering"],
#tab-alert-email:checked ~ .tabs .tab[for="tab-alert-email"],
#tab-api-keys:checked ~ .tabs .tab[for="tab-api-keys"],
#tab-identity:checked ~ .tabs .tab[for="tab-identity"],
#tab-outgoing-mail:checked ~ .tabs .tab[for="tab-outgoing-mail"],
#tab-housekeeping:checked ~ .tabs .tab[for="tab-housekeeping"],
#tab-costs:checked ~ .tabs .tab[for="tab-costs"],
#tab-account:checked ~ .tabs .tab[for="tab-account"],
#tab-ai-connectors:checked ~ .tabs .tab[for="tab-ai-connectors"] {
  box-shadow: var(--shadow-lift);
}

/* ── a rail, not a strip, on a screen with room for one ─────────────────────
   ⚠ DIFFERENT STRUCTURE, NOT ONLY A DIFFERENT SKIN. Every screen built on
   this exact radio + .tabs + .tabpanel machinery picks this up together -
   Settings here, and the admin's own Site/Pricing screen, which renders the
   identical markup under a different radio name (_sitetab, not _tab). The
   test is structural (a form whose direct children include a .tabpanel),
   never a class naming one screen, so a third screen built the same way
   inherits this for free the way page-head icons did earlier.
   None of the :checked selectors above move or change - they only ever set
   colour, and colour applies the same whether the pill they are painting
   sits in a row or a column.
   1151px, not 1150px: the row-with-horizontal-scroll treatment a few hundred
   lines up owns everything at 1150px and under, and the two must never both
   claim the same width. */
@media (min-width: 1151px) {
  form:has(> .tabpanel) {
    display: grid; grid-template-columns: 234px minmax(0, 1fr);
    align-items: start; gap: 4px 32px;
  }
  /* ⚠ REPORTED LIVE: the rail's own bottom edge overlapping the Save bar,
     hiding the last tab (Account) behind it. Both are independently
     position: sticky - this one pinned top: 20px, .sticky-save pinned
     bottom: 0 (screens.css, "One save bar, pinned to the bottom") - and
     sticky positioning alone never stops two independent sticky elements
     from occupying the same screen region at once on a short viewport or a
     tall tab list. .sticky-save carries z-index: 5 and this rail carries
     none, so wherever they overlapped, the save bar painted over the rail
     rather than the reverse. The fix is capping how tall the rail is
     allowed to get and letting IT scroll internally, rather than trying to
     out-rank the save bar in z-index - a rail an operator cannot fully see
     is no better than one a button is drawn over. */
  form:has(> .tabpanel) > .tabs {
    grid-column: 1; grid-row: 1 / -1;
    flex-direction: column; flex-wrap: nowrap; align-items: stretch;
    gap: 4px; position: sticky; top: 20px;
    max-height: calc(100vh - 100px); overflow-y: auto;
    background: var(--surface); border: 1px solid var(--line);
    border-radius: var(--radius); padding: 10px; box-shadow: var(--shadow);
  }
  form:has(> .tabpanel) > .tabs .tab {
    justify-content: flex-start; border-radius: var(--radius-sm);
    border-color: transparent; background: none; box-shadow: none;
  }
  form:has(> .tabpanel) > .tabs .tab:hover { background: var(--surface-2); }
  form:has(> .tabpanel) > .tabpanel { grid-column: 2; }
  form:has(> .tabpanel) > .actions.sticky-save { grid-column: 1 / -1; }
  /* The row-mode pills carry their own shadow above; a rail item sits flush
     against a card that already has one, so it goes back to none here -
     otherwise every resting item doubled the card's own edge. */
  #tab-behaviour:checked ~ .tabs .tab[for="tab-behaviour"],
  #tab-incoming-mail:checked ~ .tabs .tab[for="tab-incoming-mail"],
  #tab-filing:checked ~ .tabs .tab[for="tab-filing"],
  #tab-filtering:checked ~ .tabs .tab[for="tab-filtering"],
  #tab-alert-email:checked ~ .tabs .tab[for="tab-alert-email"],
  #tab-api-keys:checked ~ .tabs .tab[for="tab-api-keys"],
  #tab-identity:checked ~ .tabs .tab[for="tab-identity"],
  #tab-outgoing-mail:checked ~ .tabs .tab[for="tab-outgoing-mail"],
  #tab-housekeeping:checked ~ .tabs .tab[for="tab-housekeeping"],
  #tab-costs:checked ~ .tabs .tab[for="tab-costs"],
  #tab-account:checked ~ .tabs .tab[for="tab-account"],
  #tab-ai-connectors:checked ~ .tabs .tab[for="tab-ai-connectors"] {
    box-shadow: none;
  }
}

/* --- inputs with a leading icon ------------------------------------------ */
.input-icon { position: relative; display: block; }
.input-icon .ico {
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  color: var(--ink-3); pointer-events: none;
}
/* ⚠ `input[type]`, not `input`, and the attribute is doing real work.

   This rule reserves the space the icon sits in. console.css opens with
   `input[type=text], input[type=email], … { padding: .7rem .85rem }` — a
   SHORTHAND, so it resets padding-left along with the rest — and that selector
   scores (0,1,1). So did `.input-icon input`. Identical specificity, and
   console.css is loaded second, so it won every time: the icon then sat on top
   of the field's own text in every icon-prefixed input on the settings screen.
   Outgoing mail, API keys, the confidence threshold, the spam score — all of
   them, reading as one illegible smudge at the left of the box.

   `.input-icon input[type]` scores (0,2,1) and wins on merit rather than on
   which sheet happens to load last. Every input here carries a type, so
   nothing is lost by matching on it. `select` needs no such help: it is a bare
   type selector in console.css, which .input-icon select already outranks. */
.input-icon input[type] { padding-left: 40px; }
.input-icon select      { padding-left: 40px; }
.input-icon input:focus ~ .ico { color: var(--brand); }

/* --- sticky save --------------------------------------------------------- */
/* The settings form is long; the save button should not require scrolling. */

button .ico, .btn .ico { margin-right: -2px; }


.ico-lg { width: 22px; height: 22px; }
.ico-xl { width: 34px; height: 34px; opacity: .55; }
.empty .big { display: block; margin-bottom: 10px; }
.bar nav a span, .tab span { display: inline-block; }
button.link { gap: 6px; }

/* --- layout corrections -------------------------------------------------- */
/* Six nav items plus the brand and the mode pill wrapped onto a second row
   at common laptop widths. Tightening the nav keeps it on one line without
   shrinking the text below a comfortable size. */
.bar-inner { gap: 18px; }
.bar nav { gap: 0; }
.bar nav a { padding: 8px 11px; gap: 6px; }

/* The sticky save bar floats over the form, so the form needs room beneath
   its last field or that field sits permanently behind the button. */

/* The nav must never wrap: a second row pushes the page content down and
   makes the header look broken. It scrolls horizontally instead on the
   rare width where it genuinely cannot fit. */
.bar nav { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
.bar nav::-webkit-scrollbar { display: none; }

/* A translucent sticky bar lets text show through from behind, which reads
   as a rendering fault. Solid, with a top edge, so it is clearly a layer. */
.tabpanel { padding-bottom: 24px; }

/* nowrap on the container stops the row breaking, but each item must also
   refuse to break internally or "Needs you" becomes two lines. */
.bar nav a { white-space: nowrap; }
.tab { white-space: nowrap; }

/* Match the content column rather than bleeding past it. */
/* One save bar, pinned to the bottom of the viewport. Solid rather than
   translucent: text showing through from behind reads as a rendering fault. */
.sticky-save {
  position: sticky; bottom: 0; z-index: 5;
  background: var(--surface);
  border-top: 1px solid var(--line);
  border-radius: var(--radius) var(--radius) 0 0;
  box-shadow: 0 -4px 16px rgba(16,24,32,.06);
  margin: 0; padding: 14px 20px;
}
/* A field focused near the foot of the form must not end up under the bar when
   the browser scrolls it into view. :has() is already a dependency of this
   sheet; where it is unsupported the rule drops and behaviour is unchanged. */
form:has(.sticky-save) :is(input, select, textarea, .field, label) { scroll-margin-bottom: 6rem; }
.wrap, .col, main { scroll-padding-bottom: 6rem; }

#tab-account:checked ~ .tabs .tab[for="tab-account"] { background: var(--ink); border-color: var(--ink); color: #fff; }
#tab-account:checked ~ #panel-account { display: block; }
.tabpanel h2 { margin-top: 0; display: flex; align-items: center; gap: 9px; }

/* --- account button in the bar ------------------------------------------- */
.iconbtn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: var(--radius-sm);
  color: var(--ink-2); border: 1px solid transparent; flex: none;
}
.iconbtn:hover { background: var(--surface-2); color: var(--ink); text-decoration: none; }
/* Visible label for screen readers only - the icon alone is not a name. */
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden;
      clip: rect(0 0 0 0); white-space: nowrap; }

/* The save bar belongs to the settings form. On the Account tab the only
   action is Change password, and a second primary button there implies
   the password is saved by the wrong one. AI connectors has no fields of
   this form's at all - it is a card that links out - so the same is true
   there for an even plainer reason. */
#tab-account:checked ~ .sticky-save,
#tab-ai-connectors:checked ~ .sticky-save { display: none; }

/* --- per-row quick actions ----------------------------------------------- */
.rowactions { text-align: right; white-space: nowrap; }
.rowactions button { padding: 4px 9px; font-size: 13px; }
/* Delete is destructive, so it reads as such - but quietly, since it sits
   on every row and a wall of red would drown the actual warnings. */
tbody tr:hover .rowactions button { opacity: 1; }
.rowactions button { opacity: .65; transition: opacity .12s ease; }

/* --- row actions: icons with a hover label -------------------------------- */
/* The label is a CSS pseudo-element, not a title attribute: native tooltips
   take about a second to appear and cannot be styled. It sits to the LEFT
   of the icon because the table lives in an overflow container, and a
   tooltip drawn above or to the right would be clipped by it. */
.iconaction {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0; margin-left: 2px;
  background: none; border: 1px solid transparent; border-radius: var(--radius-sm);
  color: var(--ink-3); cursor: pointer; opacity: .7;
  transition: opacity .12s ease, color .12s ease, background .12s ease;
}
tbody tr:hover .iconaction { opacity: 1; }
/* A guard that can be seen. The delete on a default mailbox is refused, and
   used to be refused by not being drawn - which on a new account, where the
   only mailbox is the default, meant the option existed nowhere. Dimmer than
   the live one, and it keeps that dimness on hover so it does not read as
   something that will respond. */
.iconaction[disabled] { opacity: .3; cursor: not-allowed; }
tbody tr:hover .iconaction[disabled] { opacity: .3; }
.iconaction .ico { width: 17px; height: 17px; }

.iconaction::after {
  content: attr(data-label);
  position: absolute; right: calc(100% + 6px); top: 50%; transform: translateY(-50%);
  background: var(--ink); color: var(--surface);
  font-size: 12px; font-weight: 600; line-height: 1; white-space: nowrap;
  padding: 6px 9px; border-radius: 6px;
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .12s ease;
}
.iconaction:hover::after, .iconaction:focus-visible::after { opacity: 1; visibility: visible; }

/* Colour carries the meaning: dismiss is a filing action, delete destroys. */
.act-dismiss:hover { color: var(--info); background: var(--info-soft); border-color: var(--info); }
.act-delete:hover  { color: var(--bad);  background: var(--bad-soft);  border-color: var(--bad); }
.act-dismiss:hover::after { background: var(--info); }
.act-delete:hover::after  { background: var(--bad); }

#tab-housekeeping:checked ~ .tabs .tab[for="tab-housekeeping"] { background: var(--ink); border-color: var(--ink); color: #fff; }
#tab-housekeeping:checked ~ #panel-housekeeping { display: block; }

#tab-costs:checked ~ .tabs .tab[for="tab-costs"] { background: var(--ink); border-color: var(--ink); color: #fff; }
#tab-costs:checked ~ #panel-costs { display: block; }

/* The costs page filters with links rather than radios, so its tabs need the
   selected state applied directly. */
a.tab { text-decoration: none; }
a.tab:hover { text-decoration: none; }
a.tab.tab-on {
  color: var(--brand-ink); background: var(--surface);
  border-color: var(--line); border-bottom: 1px solid var(--surface);
}
.card .n { word-break: break-word; }

/* =========================================================================
   RESPONSIVE
   =========================================================================
   Three widths, defined once here rather than scattered through the file:

     >= 1151px   everything inline, nav labels shown
     901-1150px  nav goes icon-only; nine items no longer fit with labels
     <= 900px    the nav collapses behind a menu button
     <= 560px    tables stop being tables

   The menu is a checkbox and a sibling selector - the same mechanism the
   settings tabs use, and for the same reason: no control in this interface is
   allowed to need script to work, and the one script that is loaded only marks
   a pressed button busy. There is nothing here to open a menu with. A
   <details> element was the obvious alternative and was not used, because
   forcing its content to stay visible above the breakpoint means overriding
   UA-stylesheet behaviour that engines do not implement identically.
   ========================================================================= */

/* The toggle itself is never seen; only its label is. */
.navtoggle { position: absolute; opacity: 0; pointer-events: none; }
.navtoggle-btn { display: none; }

@media (max-width: 1150px) {
  .bar nav a span { display: none; }
  .bar nav a { padding: 8px 10px; }
  .bar nav a.on span { display: inline-block; }
}

@media (max-width: 900px) {
  .bar-inner { flex-wrap: wrap; gap: 12px; padding: 8px 16px; min-height: 56px; }
  /* Menu button last, hard against the right edge - where a thumb reaches it
     without crossing the screen. The auto margin sits on the group before it
     so the two are pushed across together; the button itself never shrinks and
     never wraps, because it is the only way back to the rest of the site. */
  .brand { order: 1; }
  .bar-right { order: 2; margin-left: auto; min-width: 0; }
  .navtoggle-btn { order: 3; flex: none; }
  .bar nav { order: 4; }

  /* The word beside the logout icon is the least useful thing in the bar and
     the easiest 60px to find. Hidden from here down, not just on a phone, so
     the menu button always has room. The button keeps its aria-label. */
  .bar-right .link span { display: none; }
  .bar-right { gap: 6px; }

  /* A hamburger drawn in CSS: no icon file, no extra request, and it inherits
     currentColor so it works in both themes. */
  .navtoggle-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 42px; height: 42px; flex: none; cursor: pointer;
    border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
    color: var(--ink-2);
  }
  .navtoggle-btn:hover { background: var(--surface-2); color: var(--ink); }
  .navtoggle-bars, .navtoggle-bars::before, .navtoggle-bars::after {
    display: block; width: 18px; height: 2px; border-radius: 2px;
    background: currentColor; content: "";
  }
  .navtoggle-bars { position: relative; }
  .navtoggle-bars::before { position: absolute; top: -6px; }
  .navtoggle-bars::after  { position: absolute; top: 6px; }
  /* Focus has to be visible on the label, because the input it controls is
     the thing that actually receives focus and it is invisible. */
  .navtoggle:focus-visible ~ .navtoggle-btn { outline: 2px solid var(--brand); outline-offset: 2px; }

  .bar nav {
    display: none;
    width: 100%; flex-direction: column; gap: 2px;
    padding: 6px 0 4px; margin-top: 4px;
    border-top: 1px solid var(--line);
    overflow: visible;                 /* it is a column here, nothing to scroll */
  }
  .navtoggle:checked ~ nav { display: flex; }

  /* Labels come back the moment there is a full row for each item. Hiding
     them was only ever a way to fit nine things across one bar. */
  .bar nav a { padding: 11px 12px; min-height: 44px; }
  .bar nav a span, .bar nav a.on span { display: inline-block; }
  .bar nav .navcount { margin-left: auto; }

  main { padding: 20px 16px 56px; }
  h1 { font-size: 22px; }
  /* ⚠ .page-head h1 outranks the plain h1 rule above on specificity alone
     (0,1,1 beats 0,0,1), which would otherwise carry the desktop 30px onto a
     phone regardless of this media query. Restated here, smaller than the
     desktop size but still a step above a plain heading. */
  .page-head h1 { font-size: 24px; }
  .card .n { font-size: 27px; }

  /* The tab strips scroll rather than wrapping onto a second row, which would
     push the panel down and read as a broken header. */
  .tabs { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
          scroll-snap-type: x proximity; }
  .tabs::-webkit-scrollbar { display: none; }
  .tab { scroll-snap-align: start; }
}

@media (max-width: 900px) {
  /* Touch targets. 44px is the smallest a finger reliably hits, and a tablet
     is as much a touch device as a phone. */
  button, .btn { min-height: 44px; padding: 11px 18px; }
  .iconaction { width: 40px; height: 40px; opacity: 1; }
  tbody tr .iconaction { opacity: 1; }
  /* There is no hover on a touch screen, so the label would never appear.
     Each button carries an aria-label, which is what actually names it here. */
  .iconaction::after { display: none; }

  /* --- tables become cards ---------------------------------------------
     Below this width a table can only be scrolled sideways, which hides the
     column that matters behind the edge of the screen. 900 rather than a
     phone-sized figure because the message list carries nine columns: at 768,
     an iPad held upright, the last three fall off. Each row becomes a card and
     each cell carries its own heading, taken from the data-label attribute the
     views set. Nothing is hidden and nothing needs panning. */
  .tablewrap {
    overflow-x: visible; border: 0; background: none; box-shadow: none;
    border-radius: 0;
  }
  table, tbody, tr, td { display: block; width: 100%; }
  thead { display: none; }

  tbody tr {
    background: var(--surface); border: 1px solid var(--line);
    border-radius: var(--radius); box-shadow: var(--shadow);
    margin: 0 0 10px; padding: 4px 0; overflow: hidden;
  }
  tbody tr:hover td { background: transparent; }
  tbody tr + tr td { border-top: 0; }

  td {
    display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap;
    padding: 8px 14px; border: 0; text-align: right;
  }
  td + td { border-top: 1px solid var(--line); }
  /* The label pushes everything else to the right with an auto margin rather
     than the cell using space-between. A cell often holds more than one thing
     - an address and a "asked for a person" pill, two action buttons - and
     space-between would spread those apart across the row instead of keeping
     them together as one value. */
  td[data-label]::before {
    content: attr(data-label);
    flex: none; text-align: left; margin-right: auto;
    color: var(--ink-3); font-size: 11px; font-weight: 650;
    letter-spacing: .06em; text-transform: uppercase;
  }
  /* nowrap exists to keep columns from breaking. There are no columns here,
     and keeping it truncates long values against the right edge instead. */
  td.nowrap, td.muted { white-space: normal; }
  td .subj, td .from { text-align: right; word-break: break-word; }
  .rowactions { text-align: right; }
  /* The bar is a flex child of .conf, which is itself a flex child of the
     cell. Without a basis of its own it collapses to nothing and the row shows
     a bare percentage - which is the one thing the bar exists to give context
     to. */
  td .conf { flex: 1 1 auto; min-width: 0; }
  .conf-track { max-width: none; }

  td.rowactions { gap: 8px; }
}

@media (max-width: 560px) {
  main { padding: 16px 12px 56px; }

  /* ⚠ 16px, not 14px. iOS Safari zooms the whole page in when a focused field
     has text smaller than 16px, and it does not zoom back out afterwards -
     which leaves the operator on a page they now have to pan sideways. This is
     the one place where the type scale gives way to the platform. */
  input[type=text], input[type=password], input[type=email], input[type=number],
  input[type=search], select, textarea { font-size: 16px; padding: 11px 12px; }
  textarea { font-size: 14px; }

  .actions { gap: 8px; }
  .actions button, .actions .btn { flex: 1 1 auto; justify-content: center; }

  .meta { grid-template-columns: 1fr; gap: 12px; }
  .group-heading { margin-top: 26px; }
  .sticky-save { padding: 12px 14px; margin: 0 -12px; border-radius: 0; }
}

/* =========================================================================
   DISCLOSURES
   =========================================================================
   <details> where it is valid, which is everywhere except inside a table row -
   a <tr> may contain only <td> and <th>, so the message list uses a checkbox
   instead (see .rowmore below). No JavaScript is involved in either.
   ========================================================================= */

summary { cursor: pointer; list-style: none; }
summary::-webkit-details-marker { display: none; }
summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: var(--radius-sm); }

/* One marker, drawn in CSS and rotated when open, so both engines' native
   markers can be turned off and the result looks the same in each. */
summary::after {
  content: ""; flex: none; margin-left: auto;
  width: 8px; height: 8px; border-right: 2px solid var(--ink-3); border-bottom: 2px solid var(--ink-3);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .15s ease;
}
details[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }

/* --- the mode banner on the dashboard ------------------------------------ */
.status > summary {
  display: flex; align-items: center; gap: 16px;
  padding: 16px 20px;
}
.status > summary:hover { background: var(--surface-2); }
.status .status-title { font-weight: 650; font-size: 16px; margin: 0; }
.status .status-body { margin: 0; padding: 0 20px 16px 60px; }
.status .status-icon { margin: 0; }

/* --- the CSS-only modal, shared by every popup on either panel -----------
   ⚠ A hidden checkbox plus two <label for>s open and close it — whatever
   opens the modal, and the modal's own scrim and close glyph, all pointing
   at the same checkbox id. Unchecking it or checking it is the only state
   involved, so no script anywhere in either panel has to run for a popup
   to open or shut. Scoped by nesting, not by a unique class per instance:
   `.modal-toggle:checked ~ .modal` only ever matches within whichever
   parent element the toggle and the modal share, so the same three classes
   serve every popup on the page independently.

   First built for the media-library "Change" popup on a Pictures field
   (apps/saas/admin/views/site.php, admin-only); reused as-is for the
   dashboard's mode-switch confirmation (apps/console/views/dashboard.php,
   shared with on-premise) — which is why this lives in screens.css and not
   console.css: the second use needed it somewhere on-premise loads too. */
.modal-toggle {
  /* ⚠ VISUALLY HIDDEN, NOT `hidden`/display:none. The .disclose pattern
     this is modelled on gets keyboard support for free from
     <details>/<summary>; a checkbox does not, and removing it from the
     layout with `hidden` also removes it from the tab order — nothing to
     Tab to, nothing Space would toggle. Clipped to a point instead: still
     painted, still focusable, still answers Space, just with nothing on
     screen at its own position. */
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
/* ⚠ visibility, not display:none. display cannot be transitioned - a popup
   toggled that way either snaps open with no motion or, worse, animates OUT
   by staying laid out and clickable for the length of the transition. The
   scrim fades and the body settles in from a slight drop and scale; closing
   runs the same motion in reverse. visibility carries a delayed transition
   of its own so the modal leaves the accessibility tree (and the tab order)
   only once the fade has actually finished, not the instant it starts. */
.modal {
  position: fixed; inset: 0; z-index: 60;
  visibility: hidden; opacity: 0; pointer-events: none;
  transition: opacity .2s ease, visibility 0s linear .2s;
}
.modal-toggle:checked ~ .modal {
  visibility: visible; opacity: 1; pointer-events: auto;
  transition: opacity .2s ease, visibility 0s linear 0s;
}
.modal-scrim { position: absolute; inset: 0; background: rgba(18,22,31,.5); cursor: pointer; }
.modal-body {
  position: relative; z-index: 1; max-width: 34rem; max-height: 82vh; overflow: auto;
  margin: 6vh auto 0; padding: 1.5rem 1.75rem; border-radius: var(--r-lg, 1rem);
  background: var(--ground); border: 1px solid var(--stroke); box-shadow: var(--shadow);
  transform: translateY(10px) scale(.97);
  transition: transform .22s cubic-bezier(.22,.68,.28,1);
}
.modal-toggle:checked ~ .modal .modal-body { transform: none; }
@media (prefers-reduced-motion: reduce) {
  .modal { transition: opacity .01s linear, visibility 0s linear; }
  .modal-body { transition: none; }
}
.modal-close {
  position: absolute; top: .9rem; right: .9rem; cursor: pointer;
  display: flex; padding: .3rem; border-radius: 50%; color: var(--muted);
  transition: background .16s var(--ease, ease), color .16s var(--ease, ease);
}
.modal-close:hover { background: var(--ground-2); color: var(--ink); }
.modal-close .ico { width: .9rem; height: .9rem; }

/* ── the plan-limit popup: an upsell, not an error dialog ────────────────
   Same .modal-body every popup uses; centred and given a mark of its own so
   it reads as an invitation to a bigger plan rather than a warning that
   something went wrong - the tile that opens it isn't an error state
   either, see .mesh-add.is-limit. */
.modal-upsell { text-align: center; }
.modal-upsell-ico {
  display: grid; place-items: center; width: 56px; height: 56px; margin: 0 auto .9rem;
  border-radius: 50%; background: var(--warn-soft); border: 1px solid var(--warn);
  color: var(--warn);
}
.modal-upsell-ico .ico { width: 24px; height: 24px; }
.modal-upsell-dots { display: flex; justify-content: center; gap: 6px; margin: 0 0 .9rem; }
.modal-upsell-dots i { width: 8px; height: 8px; border-radius: 50%; background: var(--warn); }
.modal-upsell .actions { justify-content: center; }

/* --- section headings that open and close -------------------------------- */
.disclose { margin: 0 0 4px; }
.disclose > summary { display: flex; align-items: center; gap: 10px; }
/* The heading keeps its own semantics; the summary is only the control. */
.disclose > summary h2 { margin: 0; display: flex; align-items: center; gap: 8px; }
.disclose > summary h2 svg { width: 16px; height: 16px; flex: none; opacity: .7; }
.disclose > summary:hover h2 { color: var(--brand-ink); }
.disclose > summary::after { margin-left: 8px; }
/* ⚠ TWO ARROWS. The bare `summary::after` above draws a marker on EVERY
   <summary> on the page, and `.disclose > summary h3::before` further down
   draws a second one for the h3 form of this component — so every collapsible
   section headed by an h3 rendered "> Heading v", pointing two ways at once.
   The h3 keeps its own, because it sits with the text and turns with it; the
   generic one is switched off wherever that marker already exists. The h2 form
   has no ::before and is untouched. */
.disclose > summary:has(h3)::after { content: none; }

/* =========================================================================
   COLLAPSIBLE MESSAGE ROWS  (card layout only)
   =========================================================================
   Five messages stacked as cards ran to several screens. Collapsed, a card
   shows what you scan by - sender, subject, status, when - and the rest opens
   on demand.
   ========================================================================= */
.rowmore { position: absolute; opacity: 0; pointer-events: none; }
.rowmore-btn { display: none; }

@media (max-width: 900px) {
  .rowmore-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 34px; height: 34px; flex: none; margin-left: 8px; cursor: pointer;
    border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
    color: var(--ink-3);
  }
  .rowmore-btn:hover { background: var(--surface-2); color: var(--ink); }
  .rowmore-btn .ico { transition: transform .15s ease; }
  .rowmore:focus-visible ~ .rowmore-btn { outline: 2px solid var(--brand); outline-offset: 2px; }

  /* :has() reaches the cells that are siblings of the one holding the
     checkbox, which a sibling combinator alone cannot do. */
  tbody tr td.more { display: none; }
  tbody tr:has(.rowmore:checked) td.more { display: flex; }
  tbody tr:has(.rowmore:checked) .rowmore-btn .ico { transform: rotate(180deg); }

  /* The subject cell now holds the toggle as well. It must not wrap: with
     wrapping on, a long subject pushed the chevron onto a line of its own,
     left-aligned under the text, where it reads as a stray control rather than
     this row's. The subject shrinks instead and wraps within itself. */
  td[data-label="Subject"] { flex-wrap: nowrap; align-items: center; }
  td[data-label="Subject"] .subj { min-width: 0; }
}

/* =========================================================================
   RESPONSIVE SWEEP
   =========================================================================
   The things that only misbehave once the viewport is narrow enough, or once
   the content is real rather than tidy: a customer's 90-character URL, a
   forwarded booking confirmation, a log line with no spaces in it.
   ========================================================================= */

/* Nothing may push the page sideways. An image with intrinsic width larger
   than its column is the classic way that happens. */
img { max-width: 100%; height: auto; }

/* Inline code carries file paths and pipe commands - "|/home/USER/mailai/
   pipe.php contact@airgo.mk" is wider than a phone and has no space in it to
   break at. */
code, .mono { overflow-wrap: anywhere; }

/* Long unbroken URLs a customer pasted, and quoted-printable soup. */
.emailbody { overflow-wrap: anywhere; }

@media (max-width: 900px) {
  /* Half a phone screen of log is enough to scroll through; the box scrolls
     inside itself, so a taller one just buries whatever follows it. */
  .logbox { max-height: 320px; font-size: 12px; }

  /* The mode banner indents its body to clear the icon. At this width that
     indent costs more than the alignment is worth. */
  .status .status-body { padding-left: 20px; }
}

@media (max-width: 560px) {
  .login-wrap { padding: 16px; }
  .login-card { padding: 26px 20px; }

  /* Two counts across rather than one: five cards in a single column is a
     screen and a half before the recent messages start. */
  .cards { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  .card { padding: 14px 12px; }
  .card .sub { font-size: 11px; }

  /* Section headings sit closer to their content when there is less of it. */
  h2 { font-size: 17px; }
  .panel { padding: 14px 14px; }

  /* A wide select with a long model id was widening the page on the settings
     and mailbox forms. */
  select { max-width: 100%; }
}

/* --- empty states -------------------------------------------------------- */
/* An empty list is a normal state here, not a failure, so it gets the same
   care as a full one: a badge rather than a stray grey glyph, a headline that
   says which kind of empty this is, and a way onward. Without the last one a
   new workspace is a set of dead ends. */
/* ⚠ A flat circle read as a status glyph, not a place - closer to a warning
   triangle than to "there is nothing here yet, and here is what goes here."
   Two rotated, brand-tinted cards fanned behind the badge borrow the shape
   of a stack waiting to be filled (an empty folder, an empty tray) without
   drawing a literal folder for every call site: it is two pseudo-elements
   on the one span every empty_state() already renders, so every screen that
   calls it gets the same lift with no markup change and nothing hand-drawn
   to keep in sync with a rebrand. Colours are var(--brand) tints, same as
   everywhere else - a workspace that repaints its palette repaints this. */
.empty-badge {
  /* z-index: 0, not just position: relative — it OWNS a stacking context, so
     the z-index: -1 fan below stays pinned behind this badge specifically
     rather than escaping to whatever the nearest positioned ancestor is
     (a .card, a .panel) and painting behind THAT instead. */
  position: relative; z-index: 0;
  display: inline-flex; align-items: center; justify-content: center;
  width: 60px; height: 60px; margin-bottom: 22px;
  border-radius: 20px;
  background: var(--brand-soft); color: var(--brand);
  box-shadow: 0 10px 22px -12px color-mix(in srgb, var(--brand) 45%, transparent);
}
.empty-badge::before, .empty-badge::after {
  content: ""; position: absolute; z-index: -1;
  width: 46px; height: 46px; border-radius: 14px;
  background: color-mix(in srgb, var(--brand) 16%, var(--surface));
}
.empty-badge::before { top: -10px; left: -14px; transform: rotate(-16deg); }
.empty-badge::after   { top: -6px; right: -16px; transform: rotate(13deg);
  background: color-mix(in srgb, var(--brand) 9%, var(--surface)); }
.empty-good .empty-badge {
  background: color-mix(in srgb, var(--good) 12%, transparent);
  color: var(--good);
  box-shadow: 0 10px 22px -12px color-mix(in srgb, var(--good) 45%, transparent);
}
.empty-good .empty-badge::before { background: color-mix(in srgb, var(--good) 16%, var(--surface)); }
.empty-good .empty-badge::after  { background: color-mix(in srgb, var(--good) 9%, var(--surface)); }
.empty-badge .ico { width: 26px; height: 26px; opacity: 1; position: relative; }
.empty-head {
  display: block; font-size: 16px; color: var(--ink); margin-bottom: 4px;
}
.empty-body {
  margin: 0 auto; max-width: 46ch; color: var(--ink-3); font-size: 14px;
}
.empty-actions {
  display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;
  margin-top: 18px;
}

/* --- header identity ----------------------------------------------------- */
/* Two things the kit's compiled sheet used to be doing for these by accident,
   now that the console no longer loads it: a button label must never wrap, and
   a long sign-in address must not break mid-word into "demo@example. com". */
.btn, button, .bar .right .btn { white-space: nowrap; }


/* --- connecting a mailbox -------------------------------------------------
   A segmented control over the sign-in mode, and only the fields that mode
   needs. Thirty-three settings in one list is not a form, it is an inventory.

   ⚠ Both halves are hidden by RULES THAT REMOVE, never by a default of
   display:none plus a rule that restores. :has() is what makes this work with
   no JavaScript, and where it is unsupported every rule below is dropped —
   which shows both groups. That is cluttered but complete; the other way round
   would leave half the form permanently unreachable on the same browser. */
.seg { display: grid; gap: 8px; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.seg-opt { position: relative; display: block; cursor: pointer; }
.seg-opt input {
  position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; pointer-events: none;
}
.seg-face {
  display: block; padding: 10px 12px;
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  background: var(--surface);
  transition: border-color .15s ease, background .15s ease, box-shadow .15s ease;
}
.seg-face strong { display: block; font-size: 14px; font-weight: 600; color: var(--ink); }
.seg-face small  { display: block; margin-top: 2px; font-size: 12px; line-height: 1.45; color: var(--ink-3); }
.seg-opt:hover .seg-face { border-color: var(--brand-line); background: var(--surface-2); }
.seg-opt input:checked ~ .seg-face {
  border-color: var(--brand); background: var(--brand-soft);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 18%, transparent);
}
.seg-opt input:checked ~ .seg-face strong { color: var(--brand-ink); }
/* The ring goes with the widget, so focus is drawn back on. */
.seg-opt input:focus-visible ~ .seg-face { outline: 2px solid var(--brand); outline-offset: 2px; }

/* OAuth chosen: the password pair is not what signs this mailbox in. */
.conn:has(input[name="o_SMTP_AUTH_MODE"][value="xoauth2"]:checked) .conn-pass { display: none; }
/* Anything else: the OAuth block is four fields nobody is going to fill in. */
.conn:not(:has(input[name="o_SMTP_AUTH_MODE"][value="xoauth2"]:checked)) .conn-oauth { display: none; }

/* A folded section reads as a heading, not as a heading inside a button. */
.disclose > summary { cursor: pointer; list-style: none; }
.disclose > summary::-webkit-details-marker { display: none; }
.disclose > summary h3 {
  display: flex; align-items: center; gap: 8px; margin: 18px 0 10px;
  font-size: 14px; font-weight: 650; color: var(--ink-2);
}
.disclose > summary h3::before {
  content: ""; width: 7px; height: 7px; flex: none;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(-45deg); transition: transform .18s ease;
}
.disclose[open] > summary h3::before { transform: rotate(45deg); }
.disclose > summary:hover h3 { color: var(--brand-ink); }
.disclose > summary:focus-visible h3 { outline: 2px solid var(--brand); outline-offset: 3px; border-radius: 4px; }

/* ── the referral link ────────────────────────────────────────────────────
   ⚠ SHARED, not console.css: dashboard.php's referral widget lives in
   apps/console/views/, which on-premise renders too — the guard that skips
   it there (no control plane, no site()) is a runtime check, and the class
   parity test that requires this file to dress every class a shared view
   uses does not know about runtime guards, on purpose. Was on-premise ever
   to gain plans and pricing, this is already dressed for it.

   The button is a shortcut, not the only way to get the link: the input
   stays a normal readonly text field underneath it, selectable and copyable
   by hand with no JavaScript at all. See referral.js. */
.ref-link { display: flex; gap: .5rem; align-items: stretch; }
.ref-link-input { font-family: var(--mono); flex: 1 1 auto; min-width: 0; }
/* Its own rule, not just `.ref-link .btn`: the label swaps to "Select and
   copy" on a browser with no Clipboard API, which is wider than "Copy", and a
   fixed width keeps the button from resizing the row when that happens. */
.ref-copy { flex: none; min-width: 8.5rem; }
/* ⚠ TWO ICONS IN THE MARKUP, NOT ONE SWAPPED BY SCRIPT. referral.js toggles
   which one CSS shows and a class that recolours the button; it never
   touches an SVG's own path data, the same reason .side-meter's chevron is
   a CSS rotation and not a redrawn icon. */
.ref-copy .ico-copied { display: none; }
.ref-copy.is-copied { border-color: #16a34a; color: #16a34a; }
.ref-copy.is-copied .ico-copy { display: none; }
.ref-copy.is-copied .ico-copied { display: inline-flex; color: #16a34a; animation: ref-copy-pop .32s var(--ease, ease); }
@keyframes ref-copy-pop {
  0%   { transform: scale(.5); opacity: 0; }
  60%  { transform: scale(1.15); opacity: 1; }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .ref-copy.is-copied .ico-copied { animation: none; }
}

/* --- the mailbox wizard, and the page it now lives on --------------------
   Four numbered steps, one panel at a time, no script. Same radio-and-sibling
   mechanism as the settings tabs above: a checked radio reveals its own panel
   and lights its own label.

   ⚠ Every field stays in one <form> and posts together. The steps are
   presentation; the save is atomic. Splitting them into four requests would
   mean a person who stops at step 3 has left a half-written mailbox behind.

   The form itself moved to its own screen, mailbox_wizard.php - it used to
   share mailboxes.php's default width with a table of rows above it. On its
   own page it gets a narrower, centred column instead: a wizard is one task
   at a time, and the full 1120px main width just let lines of prose run
   further than was comfortable to read. */
.wizardpage { max-width: 860px; margin: 0 auto; }

.stepradio { position: absolute; opacity: 0; pointer-events: none; }

/* A horizontal progress track: a circle per step, centred over its label,
   joined by a connecting line. Equal-width flex items are what make the line
   trick below work regardless of how long any one label is - "left: 50%;
   width: 100%" always spans exactly one item's centre to the next one's,
   whatever the text between them measures. */
.steps {
  position: relative;
  display: flex; gap: 0; margin: 4px 0 32px;
  counter-reset: step;
}
.step {
  position: relative;
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 8px; cursor: pointer; padding: 2px 6px;
}
/* The connector: drawn by every step except the last, from its own centre to
   the next one's. Sits behind the circles (z-index 0 vs. 1 below) so it reads
   as one line threading through them, not four separate segments. */
.step:not(:last-child)::after {
  content: ""; position: absolute; top: 19px; left: 50%; width: 100%; height: 2px;
  background: var(--line); z-index: 0;
}
.step-n {
  position: relative; z-index: 1;
  flex: none; width: 40px; height: 40px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--surface); border: 2px solid var(--line-strong); color: var(--ink-3);
  font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums;
  transition: background .15s ease, color .15s ease, border-color .15s ease;
}
.step-t strong { display: block; font-size: 13px; font-weight: 650; color: var(--ink-2); }
.step-t small  { display: block; margin-top: 2px; font-size: 11.5px; line-height: 1.35; color: var(--ink-3); }
.step:hover .step-n { border-color: var(--brand-line); }

.steppanel { display: none; }

/* The step currently open: written out rather than generated, because the
   selector has to name the id and a loop cannot. */
#step-1:checked ~ .steps .step[for="step-1"] .step-n,
#step-2:checked ~ .steps .step[for="step-2"] .step-n,
#step-3:checked ~ .steps .step[for="step-3"] .step-n,
#step-4:checked ~ .steps .step[for="step-4"] .step-n {
  background: var(--brand); border-color: var(--brand); color: #fff;
}
#step-1:checked ~ .steps .step[for="step-1"] .step-t strong,
#step-2:checked ~ .steps .step[for="step-2"] .step-t strong,
#step-3:checked ~ .steps .step[for="step-3"] .step-t strong,
#step-4:checked ~ .steps .step[for="step-4"] .step-t strong { color: var(--brand-ink); }

/* Completed steps: any step whose number is behind the one that is open. Six
   combinations for four steps, written out for the same reason as above. The
   circle fills solid and the digit is swapped for a check mark; the line
   trailing it turns the same colour, so the whole track reads left to right
   as "done, done, here, not yet". */
#step-2:checked ~ .steps .step[for="step-1"] .step-n,
#step-3:checked ~ .steps .step[for="step-1"] .step-n,
#step-3:checked ~ .steps .step[for="step-2"] .step-n,
#step-4:checked ~ .steps .step[for="step-1"] .step-n,
#step-4:checked ~ .steps .step[for="step-2"] .step-n,
#step-4:checked ~ .steps .step[for="step-3"] .step-n {
  background: var(--good); border-color: var(--good); color: transparent;
}
#step-2:checked ~ .steps .step[for="step-1"] .step-n::after,
#step-3:checked ~ .steps .step[for="step-1"] .step-n::after,
#step-3:checked ~ .steps .step[for="step-2"] .step-n::after,
#step-4:checked ~ .steps .step[for="step-1"] .step-n::after,
#step-4:checked ~ .steps .step[for="step-2"] .step-n::after,
#step-4:checked ~ .steps .step[for="step-3"] .step-n::after {
  content: "\2713"; position: absolute; inset: 0;
  display: grid; place-items: center; color: #fff; font-size: 15px; font-weight: 700;
}
#step-2:checked ~ .steps .step[for="step-1"]::after,
#step-3:checked ~ .steps .step[for="step-1"]::after,
#step-3:checked ~ .steps .step[for="step-2"]::after,
#step-4:checked ~ .steps .step[for="step-1"]::after,
#step-4:checked ~ .steps .step[for="step-2"]::after,
#step-4:checked ~ .steps .step[for="step-3"]::after { background: var(--good); }

#step-1:checked ~ #panel-1,
#step-2:checked ~ #panel-2,
#step-3:checked ~ #panel-3,
#step-4:checked ~ #panel-4 { display: block; }

/* ⚠ Keyboard focus. The radios are off-screen, so without this a keyboard user
   Tabs into the stepper and sees nothing move. */
#step-1:focus-visible ~ .steps .step[for="step-1"] .step-n,
#step-2:focus-visible ~ .steps .step[for="step-2"] .step-n,
#step-3:focus-visible ~ .steps .step[for="step-3"] .step-n,
#step-4:focus-visible ~ .steps .step[for="step-4"] .step-n {
  outline: 2px solid var(--brand); outline-offset: 2px;
}

/* Below the width a four-across track has room to breathe, drop the
   connecting line and let each step's number sit beside its label instead of
   above it - the same shape .step used before this redesign, kept here for
   the width it still suits. */
@media (max-width: 620px) {
  .steps { flex-wrap: wrap; row-gap: 14px; }
  .step { flex: 1 1 45%; flex-direction: row; text-align: left; gap: 10px; }
  .step:not(:last-child)::after,
  .step[for]::after { display: none; }
}

/* ── the card each step's fields sit in ───────────────────────────────────
   ⚠ THIS WAS THE COMPLAINT: fields floating loose on the page background,
   one plain paragraph of .help after another with nothing to separate one
   step's content from the page around it — "too crowded, too pale, and
   inconsistent with the rest of the app", where the rest of the app is
   dashboard, mailboxes, settings, all of it living in .panel cards. Reuses
   .panel rather than a new set of rules — same background, border and
   shadow every other screen already uses, so this page finally looks like
   it belongs to them.

   margin-bottom: 0, unlike .panel's own default: .step-go right after it
   already carries a border-top and its own top padding, and stacking both
   gaps put more air above the Back/Next row than below the last field. */
.wizard-panel { margin-bottom: 0; }

/* Next and Back are <label>s, so they need to look like the buttons they are. */
.step-go {
  display: flex; gap: 10px; flex-wrap: wrap;
  margin: 26px 0 0; padding-top: 18px; border-top: 1px solid var(--line);
}
.step-go .btn { cursor: pointer; }

/* --- required fields ------------------------------------------------------
   ⚠ Derived from the control, never written into the label by hand.

   Marking twenty-four labels with a literal asterisk would be correct exactly
   once. The twenty-fifth field somebody adds would be required and unmarked,
   and nothing would say so — which is the failure mode of every hand-kept
   list in this codebase that has since been replaced by a rule.

   So the marker follows the input's own semantics: anything the browser will
   refuse to submit empty, and anything declared required to assistive
   technology, is marked. Add a required field anywhere in either console and
   it is marked from the moment it renders.

   ⚠ aria-required is here as well as required, and the pair is deliberate.
   A control inside a closed wizard panel cannot carry `required` — the browser
   refuses to submit and cannot focus it to say why, so the button just appears
   dead. Those fields declare aria-required instead: the semantics reach a
   screen reader, the server does the validating, and this rule still marks
   them.

   The asterisk itself is decorative. The word "required" reaches assistive
   technology from the attribute, not from a pseudo-element, so nothing here
   needs to be announced twice. */
/* ⚠ A DESCENDANT match, not a list of wrappers. The first version named the
   shapes it knew — a bare control, and one inside .input-icon — and promptly
   missed the new-password field, which sits inside .pw-row. Enumerating
   wrappers is the same hand-kept list this rule exists to avoid, one level
   further down. Anywhere inside the field counts. */
.field:has([required]) > label::after,
.field:has([aria-required="true"]) > label::after {
  content: "*";
  margin-left: 4px;
  color: var(--bad);
  font-weight: 700;
}

/* A sign-in method this provider does not offer. Dimmed and inert rather than
   hidden: a person looking for OAuth should find it and be told why not, not
   wonder where it went. */
.seg-opt.is-off { cursor: not-allowed; }
.seg-opt.is-off .seg-face { opacity: .45; }
.seg-opt.is-off:hover .seg-face { border-color: var(--line-strong); background: var(--surface); }

/* ── Whose settings ───────────────────────────────────────────────────────────
   The switcher between the shared defaults and one mailbox's own. Links rather
   than a <select>: the current scope has to be readable without opening
   anything, and each entry says whether that mailbox has any settings of its
   own — which is the actual question somebody arrives at this page with. */
.scopebar { display: flex; flex-wrap: wrap; gap: 14px; margin: 0 0 20px; }
.scopebar .scope {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 9px 14px 9px 11px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink); text-decoration: none;
  transition: border-color .12s, background .12s, color .12s;
}
.scopebar .scope:hover { border-color: var(--brand); background: var(--surface-2); }
.scopebar .scope svg { width: 17px; height: 17px; flex: none; opacity: .65; }
.scopebar .scope span { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.scopebar .scope strong { font-weight: 600; font-size: 14px; }
.scopebar .scope small  { color: var(--ink-3); font-size: 12px; }
.scopebar .scope.is-on {
  border-color: var(--brand); background: var(--brand-soft); color: var(--brand-ink);
}
.scopebar .scope.is-on svg    { opacity: 1; }
.scopebar .scope.is-on small  { color: var(--brand-ink); opacity: .8; }

/* ── Use the default, or set your own ─────────────────────────────────────────
   ⚠ The radios are DIRECT children of .field and come before both things they
   control, because the sibling combinator only reaches forward and only inside
   one parent. Moving them into .ov — where they visually belong — would put
   .ov-body beyond their reach and stop the toggle working, silently.

   The body is hidden rather than removed and its inputs keep their names, so a
   field flipped to the default and back has not lost what was typed in it. The
   save route keys off the ov_ flag and never the value, so a hidden input that
   still posts cannot be mistaken for an override. */
/* A compact two-up version of .seg for the default/own choice, which appears
   on every field on the page and cannot take the full-width form. */
.seg-tight { grid-template-columns: repeat(2, minmax(0, max-content)); gap: 6px; margin: 0 0 8px; }
.seg-tight .seg-face { padding: 5px 11px; }
.seg-tight .seg-face strong { font-size: 12.5px; font-weight: 550; }

.ov-inherit {
  display: flex; align-items: center; gap: 7px; margin: 0;
  font-size: 13px; color: var(--ink-3);
}
.ov-inherit svg { width: 15px; height: 15px; flex: none; opacity: .8; }
.ov-inherit strong { color: var(--ink); font-weight: 600; }

/* :has() rather than `:checked ~`, because the radio lives inside its own
   label in a .seg control and the two things it shows and hides are outside
   that label. There is no sibling relationship to walk. */
.field.ovf:has(.ovr-f:checked) > .ov-body    { display: none; }
.field.ovf:has(.ovr-s:checked) > .ov-inherit { display: none; }

/* ── Connect-a-mailbox: how to fill it in ─────────────────────────────────────
   ⚠ Selected from the FORM, not from .conn. The guide sits outside every
   .steppanel so it survives moving between steps, and the auth radio it
   depends on is nested inside one of them. :has() on the common ancestor does
   not care about that distance; a sibling combinator could not reach at all.

   Both halves stay in the DOM. Only one is shown, so a translator sees every
   string in the page source and nothing has to be fetched to be read. */
.guide {
  margin: 22px 0 0;
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--brand);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.guide > div > h3 {
  display: flex; align-items: center; gap: 8px;
  margin: 0 0 6px; font-size: 14px; font-weight: 650; color: var(--ink);
}
.guide > div > h3 svg { width: 17px; height: 17px; flex: none; color: var(--brand); }
.guide .disclose { margin-top: 12px; }
.guide .disclose > summary h3 { margin: 0; font-size: 13.5px; }

.guide-steps {
  margin: 8px 0 0; padding: 0 0 0 20px;
  font-size: 13px; line-height: 1.6; color: var(--ink-2);
}
.guide-steps li { margin: 0 0 6px; }
.guide-steps li::marker { color: var(--brand); font-weight: 650; }

/* One at a time, chosen by the same radio that shows the fields being explained. */
.guide-oauth, .guide-smtp { display: none; }
form:has(input[name="o_SMTP_AUTH_MODE"][value="xoauth2"]:checked) .guide-oauth { display: block; }
form:not(:has(input[name="o_SMTP_AUTH_MODE"][value="xoauth2"]:checked)) .guide-smtp { display: block; }

.guide-foot { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line); }
.guide-foot .muted { margin: 0 0 10px; font-size: 13px; }
.guide-cta { display: flex; flex-wrap: wrap; gap: 8px; }

/* ── Help requests, both sides ────────────────────────────────────────────────
   The customer's list and the panel's queue render the same shapes, so the
   rules live once in the shared sheet. */
.req-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.req-head h1, .req-head h2 { margin: 0; }

.req-facts { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; margin: 0 0 14px; }
.req-facts dt { font-size: 12.5px; font-weight: 600; color: var(--ink-3); }
.req-facts dd { margin: 0; font-size: 13.5px; color: var(--ink); overflow-wrap: anywhere; }

.req-msg { margin: 10px 0 0; padding: 11px 13px; border-radius: var(--radius-sm); font-size: 13.5px; }
.req-msg p { margin: 0; line-height: 1.55; overflow-wrap: anywhere; }
.req-who { display: flex; align-items: center; gap: 8px; margin-bottom: 5px;
           font-size: 12px; font-weight: 650; color: var(--ink-3); }
/* Theirs is quiet, ours carries the brand edge: on a page of several messages
   the eye needs to find the answer, not the question it already knows. */
.req-msg-mine { background: var(--surface-2); border: 1px solid var(--line); }
.req-msg-us   { background: var(--brand-soft); border: 1px solid var(--brand-line); }
.req-mean { margin: 2px 0 12px; }

.badge {
  display: inline-block; padding: 3px 9px; border-radius: 999px;
  font-size: 12px; font-weight: 650; white-space: nowrap;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink-2);
}
/* ⚠ Colour carries the same meaning as everywhere else in the console: warn is
   waiting on us, info is in hand, good is finished. A status nobody can read at
   a glance is a list that has to be read twice. */
.badge.status-just_started { background: var(--warn-soft); border-color: var(--warn); color: var(--ink); }
.badge.status-seen         { background: var(--info-soft); border-color: var(--info); color: var(--ink); }
.badge.status-pending      { background: var(--brand-soft); border-color: var(--brand); color: var(--brand-ink); }
.badge.status-solved       { background: var(--good-soft); border-color: var(--good); color: var(--ink); }

/* ── The guide for the provider actually chosen ───────────────────────────────
   ⚠ Selected on the CHECKED RADIO, not on a class the script sets, so it works
   with scripts blocked - which is the on-premise console's normal state, and
   the one place a person has nobody to ask. One rule per provider rather than
   an attribute trick, because a provider with no rule then shows nothing at
   all instead of showing the wrong steps confidently. */

/* ── The provider picker ────────────────────────────────────────
   A <select> could not do this: an <option> renders text and nothing else, so a
   list with each service's own mark has to be radios. The radio itself is taken
   out of the flow rather than hidden, so it stays focusable and the keyboard
   still moves through the group. */
.provpick { display: grid; gap: 8px; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }

/* Starting a second mailbox from the first. A row of addresses rather than a
   dropdown, because it is links: this form is a POST and a nested form is not
   valid markup. */
.copyfrom { margin: 0 0 16px; }

/* A help note with more than one thing in it. Two kinds of mail arrive at the
   owner address and they behave differently, which a run-on paragraph hides. */
/* Same rhythm as the .help paragraph it is usually a continuation of - it
   carries both classes, and the two disagreeing made a list of descriptions
   sit tighter than the sentence introducing it. */
.help-list { margin: 8px 0 12px; padding-left: 18px; }
.help-list li { margin: 0 0 5px; font-size: 13px; color: var(--ink-3); line-height: 1.5; }
.help-list li strong { color: var(--ink); font-weight: 600; }

/* ── Deleted, not gone ────────────────────────────────────────
   A deleted mailbox reads at a glance as inactive, and still reads. Dimming the
   whole row would take the address with it, and the address is the one thing
   somebody is scanning for. */
.deletedbox { margin: 20px 0 0; }
table.is-deleted tbody td { color: var(--ink-3); }
table.is-deleted tbody td:first-child a { color: var(--ink); }

/* The editor for one. `fieldset[disabled]` is what actually stops the fields
   being changed - this only makes that visible, because a disabled input in
   this design is otherwise close enough to a live one to be worth a second
   look. Cursor included: nothing here responds to a click. */
fieldset.bare { border: 0; margin: 0; padding: 0; min-width: 0; }
fieldset.is-deleted {
  border: 0; margin: 0; padding: 0; min-width: 0;
  opacity: .62; cursor: not-allowed;
}
fieldset.is-deleted input, fieldset.is-deleted select, fieldset.is-deleted textarea,
fieldset.is-deleted button, fieldset.is-deleted label { cursor: not-allowed; }
fieldset.is-deleted .provopt > input:checked ~ .provface { border-color: var(--line); background: var(--surface-2); }
.restore-inline { display: flex; gap: 8px; margin: 0 0 18px; }
.copyfrom-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.copyfrom-list .btn { font-size: 13px; }
.provopt { position: relative; display: block; cursor: pointer; }
.provopt > input {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; opacity: 0; cursor: pointer;
}
.provface {
  display: flex; align-items: center; gap: 11px; height: 100%;
  padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); transition: border-color .12s, background .12s;
}
.provopt:hover .provface { border-color: var(--ink-3); }
.provopt > input:focus-visible ~ .provface { outline: 2px solid var(--brand); outline-offset: 1px; }
.provopt > input:checked ~ .provface { border-color: var(--brand); background: var(--brand-soft); }

/* The marks are 48×048 artwork sized down here, which is why the file's own
   width and height are stripped on the way in - a presentation attribute would
   otherwise fight this and win nothing but a 48px row. */
.provmark { width: 26px; height: 26px; flex: none; display: grid; place-items: center; }
.provmark svg { width: 100%; height: 100%; }
.provmark .ico { width: 20px; height: 20px; opacity: .55; }

.provtext { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.provtext strong { font-weight: 600; font-size: 14px; }
/* The server it will actually use. Long enough to overflow a narrow card, and
   more useful trimmed than wrapped to three lines. */
.provtext small {
  font-size: 12.5px; color: var(--ink-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.provopt > input:checked ~ .provface .provtext small { color: var(--ink); }
.guide-prov { display: none; margin: 0 0 4px; }
.guide-prov + .guide-oauth, .guide-prov + .guide-smtp { margin-top: 14px; }
.guide-prov h3 { margin-bottom: 2px; }
form:has(input[name="o_SMTP_PROVIDER"][value="gmail"]:checked) .guide-prov-gmail { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="google_workspace"]:checked) .guide-prov-google_workspace { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="outlook"]:checked) .guide-prov-outlook { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="microsoft365"]:checked) .guide-prov-microsoft365 { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="zoho"]:checked) .guide-prov-zoho { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="yahoo"]:checked) .guide-prov-yahoo { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="icloud"]:checked) .guide-prov-icloud { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="fastmail"]:checked) .guide-prov-fastmail { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="cpanel"]:checked) .guide-prov-cpanel { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="custom"]:checked) .guide-prov-custom { display: block; }

/* ── What the chosen provider publishes, under the field it belongs to ────────
   Same mechanism as the guides above, and for the same reason: the sentence is
   written by PHP so it can be translated, and revealed by the selected option so
   it needs no script. The alternative was console.js composing the sentence from
   data-attributes, which puts provider prose in a file the translators never see.

   ⚠ The values are a STARTING POINT. Zoho differs by data centre, a Microsoft
   365 tenant can have SMTP AUTH switched off entirely, and cPanel is whatever
   that one server is called - which is why providers.php carries a `varies` note
   and the host lines print it instead of claiming the value is universal. */
.sugg { display: none; }
form:has(input[name="o_SMTP_PROVIDER"][value="gmail"]:checked) .sugg-gmail { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="google_workspace"]:checked) .sugg-google_workspace { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="outlook"]:checked) .sugg-outlook { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="microsoft365"]:checked) .sugg-microsoft365 { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="zoho"]:checked) .sugg-zoho { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="yahoo"]:checked) .sugg-yahoo { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="icloud"]:checked) .sugg-icloud { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="fastmail"]:checked) .sugg-fastmail { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="cpanel"]:checked) .sugg-cpanel { display: block; }
form:has(input[name="o_SMTP_PROVIDER"][value="custom"]:checked) .sugg-custom { display: block; }

/* ── A field whose value has to come from somewhere ─────────────────────
   Not the HTML `required` attribute: blank in these boxes means "follow Settings",
   which is correct and common, so the browser must not refuse to submit them. The
   badge marks what has to RESOLVE to a value, and turns red only when neither this
   mailbox nor Settings has one. */
.needed {
  margin-left: 8px; padding: 1px 7px; border-radius: 999px;
  font-size: 11px; font-weight: 600; letter-spacing: .02em; vertical-align: 1px;
  background: var(--surface-2); color: var(--ink-3); border: 1px solid var(--line);
}
.needed.is-missing { background: var(--bad-soft); color: var(--bad); border-color: var(--bad); }
.help.is-bad { color: var(--bad); }

/* ⚠ A SUGGESTION, NOT A FAULT, and toned that way on purpose. The arrangement
   it describes works and is allowed; the note only explains why the other one is
   better. Red would say the save is about to fail, which it is not.

   ⚠ [hidden] IN THE SELECTOR, NOT JUST ON THE ELEMENT. Both the server (an
   empty owner address renders the `hidden` attribute) and console.js's own
   look() (`note.hidden = ...`) were setting it correctly, and the note
   showed on every mailbox regardless — this class's own display: flex is
   author CSS of equal specificity to the UA stylesheet's [hidden] rule, and
   author beats user-agent when specificity ties. Same fix already applied
   to optgroup[hidden] just below, missed here. */
.owner-same:not([hidden]) { display: flex; align-items: flex-start; gap: 7px; }
.owner-same .ico { flex: none; margin-top: 2px; opacity: .65; }

/* ⚠ The models of a provider that is not in use. `hidden` alone is honoured by
   most browsers on an optgroup and ignored by some, which is why the server and
   console.js also mark the group `disabled` - unselectable everywhere, invisible
   wherever the browser allows it. */
optgroup[hidden] { display: none; }

/* The generic half sits under whichever provider guide is showing, always. */
.guide-oauth > h3, .guide-smtp > h3 { padding-top: 12px; border-top: 1px solid var(--line); }

/* The note under "Follow Settings" belongs to that choice alone. Same :has()
   the password and OAuth blocks use, and for the same reason: the radio it
   depends on is nested inside the segmented control, not a sibling of this. */
.conn-follow { display: none; }
.conn:has(input[name="o_SMTP_AUTH_MODE"][value=""]:checked) .conn-follow { display: flex; }

/* ── The connector's two conditional questions ────────────────────────────
   Same :has() as the pair above, and the same reasoning. Both of these are
   questions about a choice that has not been made yet, and a form that asks
   them anyway is a form somebody reads twice to find the parts that apply.

   ⚠ HIDDEN, NOT ABSENT. Where :has() is unsupported these simply show, which
   costs two fields on a screen. Making them conditional in PHP instead would
   have meant a round trip to the server to reveal a text box, and making them
   conditional in script would have meant they never appear at all under the
   on-premise console's usual CSP. */

/* Every provider but "Any other SMTP server" fills the host names in itself,
   so asking for them is asking somebody to confirm a value they cannot see. */
.conn:not(:has(input[name="o_SMTP_PROVIDER"][value="custom"]:checked)) .conn-custom { display: none; }

/* A parent folder for a feature that is switched off is a question about
   nothing. */
.conn:not(:has(input[name="c_sort"]:checked)) .conn-sort { display: none; }

/* =========================================================================
   SETTINGS: SECTIONS AND PICKERS
   ⚠ The problem this solves is density, not decoration. A tab was a flat run
   of up to thirteen fields, every one a label, a control and a paragraph of
   grey help at the same size and weight as every other. Nothing grouped them
   and nothing ranked them, so the eye had no way in and finding one field
   meant reading all of them.
   ========================================================================= */
/* ⚠ <details>, NOT A PLAIN <div>, for every tab that declares sections
   (console_settings_sections()) - closed by default, so a page of ten
   sections opens as ten one-line headings rather than everything at once.
   A tab with nothing declared still renders as the plain .setsec-flat <div>
   it always has: two or three fields are not worth a click to see.
   The marker and its rotation are the sitewide `summary::after` /
   `details[open] > summary::after` rules a few hundred lines up - this
   component asks nothing extra of them, the same way .disclose does not. */
.setsec {
  background: var(--surface, #fff);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 0 20px 6px;
  margin: 0 0 16px;
  box-shadow: var(--shadow);
}
/* A tab with nothing declared for it renders flat, and a card round a single
   pair of fields is a box for the sake of a box. */
.setsec-flat { background: none; border: 0; padding: 0; }

/* ⚠ BLOCK, NOT FLEX, ON THE SUMMARY ITSELF - a second row (.setsec-desc)
   needed room under the heading, and the sitewide summary::after chevron
   (a few hundred lines up) only lands where flex's margin-left: auto puts
   it, which is the last item of a ROW. Made absolute here instead, so it
   still sits top-right regardless of how many rows the summary holds -
   .setsec-head-row (dot + heading) and .setsec-desc stack under it in
   ordinary block flow. */
.setsec-head {
  position: relative; display: block;
  margin: 0 0 16px; padding: 17px 40px 15px 0;
  border-bottom: 1px solid var(--line);
}
.setsec-head::after { position: absolute; top: 21px; right: 0; margin-left: 0; }
.setsec-head-row {
  display: flex; align-items: center; gap: 10px;
  font-size: 15px; font-weight: 650; color: var(--ink);
}
/* A small dot of the brand ahead of the heading - the same reasoning as
   .page-head-ico: colour alone, one glyph wide, is enough to stop a column
   of section headings from reading as identical grey rows. */
.setsec-head-row::before {
  content: ""; width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: var(--brand);
}
/* The heading keeps its own semantics (it is still an <h2>); the summary is
   only the clickable control around it, exactly as .disclose's summary/h2
   pairing works elsewhere on this screen. */
.setsec-head h2 { margin: 0; font: inherit; color: inherit; }
.setsec-head:hover h2 { color: var(--brand-ink); }
/* The one sentence a collapsed card shows without a click - muted and
   indented under the dot, so it reads as an explanation of the heading
   above it rather than as a second heading of its own. */
.setsec-desc { margin: 5px 0 0 18px; font-size: 12.5px; font-weight: 400; color: var(--ink-3); line-height: 1.4; }
.setsec-flat .setsec-head { display: none; }
.setsec .field:last-child { margin-bottom: 14px; }

/* --- cross-tab settings search -------------------------------------------
   A Stripe-style command search over every field on this screen: type
   "confidence" and land on Confidence threshold, open, on its own tab,
   without hunting through eleven sections across ten tabs first.

   ⚠ WORKS WITH NOTHING BUT ITSELF WHEN SCRIPT IS BLOCKED. This is a plain
   <input type="search"> outside the settings <form> (so Enter here can never
   submit it) - with the script blocked or absent it is simply an inert box
   that does nothing, exactly like every other enhancement on this screen.
   See settings-search.js. */
.settings-search { position: relative; margin: 22px 0 20px; max-width: 480px; }
.settings-search .input-icon input[type=search] {
  padding-top: 12px; padding-bottom: 12px; padding-right: 46px; font-size: 15px;
  box-shadow: var(--shadow);
}
.settings-search .input-icon input[type=search]:focus {
  box-shadow: var(--shadow), 0 0 0 3px color-mix(in srgb, var(--brand) 18%, transparent);
}
/* The ⌘K hint, styled as a little key-cap sitting inside the box the way
   Stripe's and Linear's own search triggers show theirs - gone the moment
   there is a real query to make room for, brought back by hide(). */
.settings-search-kbd {
  position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
  padding: .2rem .4rem; border-radius: 5px; border: 1px solid var(--line);
  background: var(--surface-2); color: var(--ink-3);
  font: 500 11px/1 var(--font-mono, monospace); pointer-events: none;
}
.settings-search:has(#settings-search-input:not(:placeholder-shown)) .settings-search-kbd,
.settings-search.is-open .settings-search-kbd { display: none; }

/* ⚠ ONE CLASS TURNS THE INLINE BOX INTO THE OVERLAY - .is-open, toggled by
   settings-search.js on focus/⌘K and removed on Escape, a backdrop click or
   picking a result. Nothing about the search or keyboard-nav logic changes;
   this only repositions the same three elements (input, results, hints) to
   the centre of the screen over a dimmed page, the shape a command search
   takes everywhere from Stripe's docs to Linear's own app. Degrades for
   free with the script blocked: .is-open is never added, so the box simply
   stays where it always sat inline, exactly as it worked before this. */
/* ⚠ NEGATIVE, NOT 89. The backdrop is a CHILD of .settings-search, and
   .settings-search.is-open is itself position:fixed with z-index:90 - which
   makes it a stacking context, so every z-index inside is only ever
   compared against its OWN siblings here, never against the page. Against
   those siblings (the input row at the implicit auto/0, the results list
   explicitly at 30) a z-index of 89 was the HIGHEST in the group, painting
   the backdrop over the results it was only ever meant to sit behind -
   reported live as "a grey overlay covering it all", which is exactly what
   a scrim on top of the text reads as. Negative puts it behind both. */
.settings-search-backdrop {
  display: none; position: fixed; inset: 0; z-index: -1;
  background: rgba(18,22,31,.45); cursor: pointer;
}
.settings-search.is-open .settings-search-backdrop { display: block; }
.settings-search.is-open {
  position: fixed; z-index: 90; top: 12vh; left: 50%;
  transform: translateX(-50%); margin: 0; width: min(560px, calc(100vw - 2rem));
  max-width: none;
}
.settings-search.is-open .input-icon input[type=search] {
  border-radius: var(--radius) var(--radius) 0 0; font-size: 16px;
}
.settings-search.is-open:not(:has(.settings-search-results:not([hidden])))
  .input-icon input[type=search] {
  border-radius: var(--radius);
}
.settings-search-results {
  position: absolute; z-index: 30; top: calc(100% + 6px); left: 0; right: 0;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow-lift);
  padding: 6px; max-height: 360px; overflow-y: auto;
}
.settings-search.is-open .settings-search-results {
  position: static; margin-top: -1px; border-top: 1px solid var(--line);
  border-radius: 0; box-shadow: none; max-height: 50vh;
}
.settings-search-hit {
  display: flex; align-items: flex-start; gap: 10px; width: 100%;
  text-align: left; padding: 9px 11px; border: 0; border-radius: var(--radius-sm);
  background: none; font: inherit; color: var(--ink); cursor: pointer;
}
/* A small round badge per result, one per hit rather than a pseudo-element -
   settings-search.js sets it as an inline mask-image the same way this
   sheet already masks the flash icons a few hundred lines up, so no SVG
   sprite is duplicated between the two. */
.settings-search-hit .ssh-ico {
  flex: none; width: 26px; height: 26px; margin-top: 1px;
  border-radius: 50%; background-color: var(--surface-2);
  display: grid; place-items: center;
}
.settings-search-hit .ssh-ico::before {
  content: ""; display: block; width: 13px; height: 13px; background-color: var(--ink-3);
  -webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='2.4'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Ccircle%20cx='11'%20cy='11'%20r='7'/%3E%3Cpath%20d='M21%2021l-4.3-4.3'/%3E%3C/svg%3E") center/contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='2.4'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Ccircle%20cx='11'%20cy='11'%20r='7'/%3E%3Cpath%20d='M21%2021l-4.3-4.3'/%3E%3C/svg%3E") center/contain no-repeat;
}
.settings-search-hit:hover .ssh-ico::before,
.settings-search-hit.is-active .ssh-ico::before { background-color: var(--brand-ink); }
.settings-search-hit:hover, .settings-search-hit.is-active { background: var(--surface-2); }
.settings-search-hit .ssh-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.settings-search-hit .ssh-label { font-weight: 600; }
.settings-search-hit .ssh-path { font-size: 12px; color: var(--ink-3); }
.settings-search-empty { padding: 10px 11px; font-size: 13px; color: var(--ink-3); }

/* The footer strip Stripe's and Linear's own overlays close with - shown
   only in the modal state, and only once there is something to navigate. */
.settings-search-hints {
  display: none; align-items: center; gap: 16px;
  padding: 8px 12px; border-top: 1px solid var(--line);
  font-size: 12px; color: var(--ink-3);
}
.settings-search-hints span { display: inline-flex; align-items: center; gap: 5px; }
.settings-search-hints kbd {
  padding: .1rem .35rem; border-radius: 4px; border: 1px solid var(--line);
  background: var(--surface-2); font: 600 11px/1.4 var(--font-mono, monospace); color: var(--ink-2);
}
.settings-search.is-open:has(.settings-search-results:not([hidden])) .settings-search-hints {
  display: flex;
}

/* --- the option cards ---------------------------------------------------
   ⚠ The radio itself is still there and still focusable — it is only moved
   out from under the pointer. `display:none` would take it out of the tab
   order too, and a choice you cannot reach from the keyboard is not a choice. */
.pickset { display: grid; gap: 8px; }
@media (min-width: 760px) {
  .pickset-provider { grid-template-columns: repeat(3, 1fr); }
}
.pickopt { display: block; cursor: pointer; }
.pickopt input {
  position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0;
}
.pickface {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 13px 14px; border: 1px solid var(--line); border-radius: 12px;
  background: var(--surface, #fff);
  transition: border-color .15s ease, background .15s ease, box-shadow .15s ease;
}
.pickopt:hover .pickface { border-color: var(--line-strong); background: var(--surface-2); }
/* ⚠ :focus-visible on the input, dressed on the face beside it. The input is
   the thing the keyboard lands on and the face is the thing anybody can see. */
.pickopt input:focus-visible + .pickface {
  outline: 2px solid var(--brand); outline-offset: 2px;
}
.pickopt input:checked + .pickface {
  border-color: var(--brand);
  background: var(--brand-soft, var(--surface-2));
  box-shadow: inset 0 0 0 1px var(--brand);
}

.pickmark {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 34px; height: 34px; border-radius: 9px;
  background: var(--surface-2); border: 1px solid var(--line);
  color: var(--ink-2);
}
/* The three model marks are the owners' own artwork on a white ground, so they
   keep one rather than sitting on the card's tint. */
.pickset-provider .pickmark { background: #fff; }
.pickmark img { display: block; width: 22px; height: 22px; object-fit: contain; }
.pickmark svg { width: 17px; height: 17px; }

.picktext { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.picktext strong { font-size: 14px; font-weight: 650; color: var(--ink); }
.picktext span { font-size: 12.5px; line-height: 1.45; color: var(--ink-3); }

/* The tick only exists on the chosen one; the ring says which that is, and the
   mark says it again for anybody who cannot see the ring. */
.picktick { flex: none; opacity: 0; color: var(--brand); transition: opacity .15s ease; }
.picktick svg { width: 16px; height: 16px; }
.pickopt input:checked + .pickface .picktick { opacity: 1; }


/* The connection test sits between the last wizard step and the save row, so it
   reads as the last thing to do before saving rather than as another field. */
.conn-test { margin-top: 18px; }
.conn-test .flash { margin-bottom: 10px; }
.conn-test .actions { margin-top: 4px; }

/* ═══════════════════════════════════════════════════════════════════════
   Spacing, weight and rhythm.

   ⚠ IN THE SHARED SHEET, NOT THE HOSTED ONE, and that is not a filing
   decision. Every component below is used by apps/console/views/, which BOTH
   products render — so a rule for it in console.css dresses the hosted panel
   and leaves the on-premise admin with the same screen undressed. That is the
   exact gap that once left 59 classes unstyled, and console_tests.php fails
   on it now.

   WHAT IT FIXES, because every line is a symptom of one of two causes.

   GLUED. Screens are built from components that each brought their own
   margin, and where two met the gap was whatever the last one declared —
   sometimes sensible, often none. A heading sat on a card, a card sat on the
   next card, and a page read as one long region with bold lines through it
   rather than as sections. So the space BETWEEN blocks becomes a property of
   the page instead of of whichever component happens to be above, and it
   cannot vary by where you are.

   PALE. White panels on a near-white ground with a border one step off the
   background: at normal brightness the cards did not read as cards, and the
   page looked like grey text floating on nothing. Border and shadow each come
   up one step, which is the difference between a surface and a smudge.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── the space between blocks belongs to the page ────────────────────── */
/* Adjacent siblings only, so the first thing on a screen still sits where it
   always did and nothing gains a margin it has to be told to drop. */
main > .flash + *,
main > .page-head + *,
main > .panel + .panel,
main > .panel + .cards,
main > .cards + .panel,
main > .cards + .queue,
main > .panel + .queue,
main > .queue + .panel,
main > .status + * { margin-top: 20px; }

/* A heading that opens a section wants the gap ABOVE it: the words belong to
   what follows them, not to what they were pushed away from. */
main > h2 { margin-top: 36px; }
main > h2:first-child { margin-top: 0; }

/* ── the head of a screen ────────────────────────────────────────────── */
/* ⚠ Dressed HERE for the first time. It is used by the shared dashboard, and
   only the hosted sheet had a rule for it — so on-premise the title, the
   sentence under it and the first card ran together with no gap at all. */
.page-head { display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap;
             margin-bottom: 30px; }
/* The same glyph the sidebar drew for the row that led here — see $icon on
   page_head(). Same circular-badge shape as .tile-ico and .mesh-mark
   elsewhere in either panel, a size up since this is the page's own
   heading rather than a card inside it. */
.page-head-ico {
  width: 44px; height: 44px; border-radius: 50%; flex: none;
  border: 1px solid var(--brand-line); display: grid; place-items: center;
  color: var(--brand-ink); background: var(--brand-soft);
  margin-top: 2px; box-shadow: var(--shadow);
}
.page-head-ico .ico { width: 20px; height: 20px; }
.page-head > div { min-width: 0; }
.page-head h1 { margin: 0; font-size: 30px; letter-spacing: -.015em; }
.page-head p  { margin: 7px 0 0; color: var(--ink-2); font-size: 14px; max-width: 62ch; }
.page-head .actions { margin-left: auto; display: flex; gap: 8px; flex-wrap: wrap; }
.page-head + .flash,
.page-head + .status { margin-top: 0; }

/* ── the status banner ───────────────────────────────────────────────── */
/* It was glued to the paragraph above it, because the dashboard opened with a
   bare <h1> and a <p> and nothing declared the gap. The view uses .page-head
   now; this is the rule that makes it true on every screen that opens with a
   banner rather than only on the one that was reported. */
.status { margin-top: 20px; }
.page-head + .status,
.flash + .status { margin-top: 0; }

/* ⚠ AND IT IS RESPONSIVE NOW. The summary is a flex row holding a 40px mark
   and a title, and on a narrow screen the title had nowhere to go: it ran
   under the chevron and clipped. Wrapping the row is wrong — a heading broken
   in two beside a mark reads as two headings — so the mark shrinks instead
   and the title keeps the line. */
.status summary { flex-wrap: wrap; cursor: pointer; }
.status .status-title { min-width: 0; overflow-wrap: anywhere; }
.status summary::-webkit-details-marker { display: none; }
@media (max-width: 559px) {
  .status .status-icon { width: 32px; height: 32px; font-size: 16px; }
  .status .status-title { font-size: 15px; }
}

/* ── the figures row ─────────────────────────────────────────────────── */
/* Four figures at 180px each need 760px to sit in one row, and between about
   560 and 760 they dropped one at a time into a ragged two-and-two. Two even
   columns is steadier to read than four uneven ones. */
@media (max-width: 760px) {
  .meta { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 20px; }
}
@media (max-width: 380px) {
  .meta { grid-template-columns: minmax(0, 1fr); }
}

/* ── less pale ───────────────────────────────────────────────────────── */
/* A card has to read as a surface sitting ON the page. One step of border and
   one of shadow is the whole change: more than that and a screen somebody
   opens every morning starts shouting. */
.card, .stat, .queue, .panel, .status { border-color: var(--line-strong); }
.card, .stat, .queue, .panel, .status { box-shadow: var(--shadow); }
/* The label under a figure was the softest grey on the palette at 12px, which
   on white is at the edge of legible. --ink-2 is the one that was measured. */
.stat .l, .stat .sub { color: var(--ink-2); }

/* ── a little more life ──────────────────────────────────────────────── */
/* ⚠ HOVER ONLY, and only where something is genuinely clickable. The cards
   already animate in on load; one that also moves when the pointer merely
   crosses it makes a still page feel unsteady. A stat card that links
   somewhere is a target, so it behaves like one. */
a.card, a.stat {
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}
a.card:hover, a.stat:hover {
  transform: translateY(-2px);
  border-color: var(--brand-line);
  box-shadow: var(--shadow-lift);
}
/* The number is the thing being read, so it is the thing that answers. */
a.card:hover .n, a.stat:hover .n { color: var(--brand-ink); }

@media (prefers-reduced-motion: reduce) {
  a.card, a.stat { transition: none; }
  a.card:hover, a.stat:hover { transform: none; }
}

/* ── a model the plan does not include ────────────────────────────────
   ⚠ ONE LINE, not a locked row per model. Fourteen greyed entries is
   fourteen things to read past to reach the six that can be chosen, and this
   product has already been through exactly that with the other providers'
   models. What somebody needs is the count, the plan that lifts it, and one
   place to press — so it is a sentence with a button on the end of it. */
.modelgate {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 10px; padding: 9px 12px;
  background: var(--brand-soft); border: 1px solid var(--brand-line);
  border-radius: var(--radius-sm); color: var(--brand-ink);
}
.modelgate > span { flex: 1 1 14rem; min-width: 0; }
.modelgate .ico { width: 15px; height: 15px; flex: none; opacity: .8; }
.modelgate .btn {
  margin-left: auto; flex: none;
  padding: 5px 11px; font-size: 12.5px; border-radius: var(--radius-sm);
}

/* ═══════════════════════════════════════════════════════════════════════
   THE DASHBOARD LANGUAGE

   Adopted from a reference design, and adopted as a VOCABULARY rather than
   as a layout: pill tabs, one oversized figure, a three-band scale with a
   marker on it, mesh-gradient cards, circular actions, generous radii on a
   light ground. What each of those says in the reference is a credit score;
   what it says here is what this product actually does, and the mapping was
   the design decision rather than the CSS.

   ⚠ IN THE SHARED SHEET, because both products render apps/console/views/.
   A rule for these in console.css would dress the hosted panel and leave the
   on-premise admin with the same screen undressed — the gap that once left
   59 classes unstyled, and that console_tests.php now fails on.

   ⚠ NO NEW FONT, NO IMAGES, NO SCRIPT. The mesh is layered radial-gradients
   and the grain is one inline SVG data URI, which `img-src 'self' data:`
   already permits — the panels' CSP forbids a third-party stylesheet, and a
   design that needs one is a design this product cannot have.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── the greeting ─────────────────────────────────────────────────────
   Two lines, the second in grey, both large. It is the one place in either
   panel that addresses a person rather than reporting a figure. */
.dash-hello { margin: 0 0 28px; }
.dash-hello h1 {
  margin: 0; font-size: clamp(26px, 3.4vw, 38px); line-height: 1.12;
  letter-spacing: -.02em; font-weight: 600; color: var(--ink);
}
.dash-hello p {
  margin: 2px 0 0; font-size: clamp(22px, 3vw, 32px); line-height: 1.15;
  letter-spacing: -.02em; font-weight: 400; color: var(--ink-3);
}

/* ── the pill tab strip ───────────────────────────────────────────────
   ⚠ LINKS, not a control. In the reference this is the product's primary
   navigation; here both panels already have a sidebar that does that job, so
   this is the row of screens you move between in a normal week — the same
   four the sidebar groups under "Daily" — promoted to where the eye lands. */
.tabpills {
  display: flex; gap: 8px; flex-wrap: wrap; margin: 0 0 26px;
}
.tabpills a {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 9px 16px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--ink-2); font-size: 14px; font-weight: 500; text-decoration: none;
  transition: border-color .18s ease, color .18s ease, background .18s ease;
  white-space: nowrap;
}
.tabpills a svg { width: 15px; height: 15px; flex: none; opacity: .6; }
.tabpills a:hover { border-color: var(--line-strong); color: var(--ink); }
.tabpills a.on {
  background: var(--ink); border-color: var(--ink); color: #fff;
}
.tabpills a.on svg { opacity: 1; }
/* The count beside a label, as in the reference's "Inquiries 24". */
.tabpills .n {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 19px; height: 19px; padding: 0 5px; border-radius: 999px;
  background: var(--brand-soft); color: var(--brand-ink);
  font-size: 11px; font-weight: 650; font-variant-numeric: tabular-nums;
}
.tabpills a.on .n { background: rgba(255,255,255,.2); color: #fff; }

/* ── the one oversized figure ─────────────────────────────────────────
   The reference gives a whole screen's attention to a single number. That
   only works if the number is genuinely the one thing you came to see, so
   each panel picks its own and nothing else on the screen competes. */
.hero-fig { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; }
.hero-fig .n {
  /* ⚠ 8vw hit the 88px ceiling on any desktop, which is headline size for a
     figure that is usually one digit. The floor is what a phone gets and was
     right; only the ceiling was wrong. */
  font-size: clamp(52px, 5vw, 60px); line-height: .95; font-weight: 600;
  letter-spacing: -.045em; color: var(--ink); font-variant-numeric: tabular-nums;
}
/* ⚠ .hero-side, NOT .side. It was `.hero-fig .side`, and `.side` is the
   SIDEBAR: position sticky, top 0, z-index 40, its own background and border.
   A descendant selector does not stop the bare `.side` rule matching too, so
   this caption became a sticky white box that followed the scroll and sat on
   top of the header. Exactly the shape of the `.tabs` collision this sheet
   already carries a warning about — a component named after a common word
   inherits whatever else claimed it first. */
.hero-fig .hero-side { display: flex; flex-direction: column; gap: 1px; }
.hero-fig .cap  { font-size: 13px; color: var(--ink-2); }
.hero-fig .cap b { font-weight: 600; color: var(--ink); }

/* ── the facts beside the figure ──────────────────────────────────────
   ⚠ The hero card was a big number on the left and nothing at all on the
   right, at any width above a phone. These are the counts that used to sit in
   a separate row of tiles below it, at the same visual weight as each other
   and as everything else on the screen. Beside the figure they read as what
   they are: the figure is what happened, these are what is left.

   margin-left:auto pushes them to the far edge on a wide card and does
   nothing once the flex parent wraps, so the phone layout is unchanged. */
.hero-facts {
  margin-left: auto; display: flex; flex-wrap: wrap; gap: 8px 30px; align-items: flex-end;
}
.hero-facts .f {
  display: flex; flex-direction: column; gap: 1px;
  text-decoration: none; color: inherit; border-radius: 10px;
  padding: 4px 8px; margin: -4px -8px;
  transition: background .18s ease, transform .3s var(--bounce, ease);
}
.hero-facts .f:hover { background: var(--ground-2, rgba(15, 23, 42, .05)); transform: translateY(-3px) scale(1.04); }
.hero-facts .fn {
  font-size: 22px; font-weight: 650; letter-spacing: -.02em;
  color: var(--ink); font-variant-numeric: tabular-nums; line-height: 1.1;
}
.hero-facts .fl { font-size: 12px; color: var(--ink-3); white-space: nowrap; }
/* The one that means somebody is waiting on a person. */
.hero-facts .f.is-need .fn { color: var(--warn, #b37a00); }

/* ── the week-over-week badge ─────────────────────────────────────────
   trend_badge() in helpers.php only ever emits this from two real counts,
   so there is nothing here to fake — the colour just has to agree with
   whichever direction that figure calls good, which is $badIfUp's job. */
.trend {
  display: inline-flex; align-items: center; gap: 2px;
  font-size: 11px; font-weight: 700; letter-spacing: 0;
  vertical-align: 1px;
}
.trend-good { color: var(--good); }
.trend-bad  { color: var(--bad); }
.trend svg { flex: none; }

@media (max-width: 700px) {
  /* Left-aligned under the figure rather than pinned right, where a two-item
     row would otherwise sit against the edge with a gap in the middle. */
  .hero-facts { margin-left: 0; width: 100%; gap: 8px 22px; }
  .hero-facts .fn { font-size: 19px; }
}

/* ── the three-band scale ─────────────────────────────────────────────
   ⚠ THE MAPPING IS THE POINT. In the reference this is a credit score across
   poor / fair / good. Here it is the three MODES, which are a progression of
   exactly the same shape: watching, helping, answering. The marker says where
   this workspace sits on a scale it is meant to move along, which is the one
   thing the old status banner said in a paragraph. */
.bands { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 22px; }
/* ⚠ Wraps a band that opens a popup instead of the .bands grid itself, so
   its checkbox and its popup have somewhere to be .band's siblings without
   becoming a fourth grid item — see dashboard.php. Neither actually would
   have (the checkbox is position: absolute, the popup position: fixed),
   but the wrapper carrying them would. */
.band-wrap { display: contents; }
.band {
  position: relative; padding: 14px 14px 26px; border-radius: 14px;
  /* ⚠ Half of these bands are <label>, pointing at a checkbox that opens a
     confirmation popup rather than submitting anything directly (a plain
     <button> once did that; see dashboard.php for why it no longer can). A
     label carries none of a button's own chrome, but it also claims no
     click of its own by default the way a button does, so most of this
     reset exists to make it look identical to the .band[aria-current] span
     it sits beside — the two must read as one object, current or not. */
  display: block; width: 100%; text-align: left;
  border: 0; font: inherit; background-clip: padding-box;
  -webkit-appearance: none; appearance: none;
}
label.band { cursor: pointer; transition: transform .12s ease, box-shadow .12s ease; }
label.band:hover { transform: translateY(-1px); box-shadow: 0 4px 14px rgba(15, 23, 42, .10); }
/* ⚠ ON THE CHECKBOX, REACHING THE LABEL. Focus lands on .modal-toggle, not
   on this label — the checkbox is the thing Tab can reach and Space can
   toggle, per the CSS-only modal's own visually-hidden-not-`hidden` rule
   (screens.css). Without this, a keyboard user seeing no focus ring on
   the visible band would have no way to tell where they were. */
.modal-toggle:focus-visible ~ label.band { outline: 2px solid var(--ink); outline-offset: 2px; }
/* The one you are in is not pressable, and says so by not moving. */
.band[aria-current] { cursor: default; }
@media (prefers-reduced-motion: reduce) {
  label.band { transition: none; }
  label.band:hover { transform: none; }
}
.band .lbl {
  position: absolute; left: 14px; bottom: 7px;
  font-size: 12px; font-weight: 500; color: var(--ink-3);
}
.band .band-icon { display: block; margin-bottom: 8px; }
.band .band-icon .ico { width: 20px; height: 20px; }
.band .dots { display: flex; align-items: center; gap: 7px; height: 12px; }
.band .dots i { width: 5px; height: 5px; border-radius: 50%; opacity: .55; }
/* The ring marking the end of a band you have passed through. */
.band .ring {
  margin-left: auto; width: 13px; height: 13px; border-radius: 50%;
  border: 2px solid currentColor; position: relative;
}
.band .ring::after {
  content: ""; position: absolute; inset: 3px; border-radius: 50%;
  background: currentColor;
}
.band-1 { background: #FDECEA; color: #C4392F; }
.band-2 { background: #FDF3DF; color: #B37A00; }
.band-3 { background: #E7F6EE; color: #0F7B4F; }
.band-1 .dots i { background: #C4392F; }
.band-2 .dots i { background: #B37A00; }
.band-3 .dots i { background: #0F7B4F; }
/* Where you are: a pointer on a stem, dropped onto the band in force. */
.band.is-here .lbl { color: var(--ink); font-weight: 650; }
.band .here {
  position: absolute; top: -12px; left: 50%;
  display: flex; flex-direction: column; align-items: center;
  animation: here-bob 1.8s ease-in-out infinite;
}
@keyframes here-bob {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50%      { transform: translateX(-50%) translateY(-4px); }
}
.band .here span {
  width: 0; height: 0; border-left: 6px solid transparent;
  border-right: 6px solid transparent; border-top: 8px solid var(--ink);
}
.band .here i { width: 1px; height: 26px; background: var(--ink); opacity: .5; }

/* ── mesh cards ───────────────────────────────────────────────────────
   The reference's signature: a soft multi-point gradient with grain over it,
   white type, a circular action in the corner.

   ⚠ LAYERED RADIAL GRADIENTS, not an image. Four soft lights over a base,
   which is what a mesh gradient is; an image would be a request, a file to
   ship and a thing that cannot follow the operator's brand colour.

   ⚠ The grain is an inline SVG data URI. It is what stops a large smooth
   gradient reading as a rendering artefact, and it is permitted by the
   panels' `img-src 'self' data:` — a texture from a CDN would not be. */
.mesh {
  position: relative; overflow: hidden; isolation: isolate;
  border-radius: 22px; padding: 20px; color: #fff;
  min-height: 148px; display: flex; flex-direction: column; gap: 10px;
  text-decoration: none;
}
.mesh::after {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  opacity: .28; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
}
.mesh > * { position: relative; z-index: 1; }
/* ⚠ DERIVED FROM THE BRAND, NOT COPIED FROM THE REFERENCE. The first version
   of these was the reference's own olive, dusty rose and slate, and that is
   the same bug as the workspace initials once had: an install that chose
   orange got three cards in somebody else's palette, on the screen a customer
   opens first. --brand is remapped to BRAND_PRIMARY by site_palette_css(), so
   everything below moves with it.

   The three stay distinguishable by mixing the brand toward three different
   deep neutrals — one warm, one green-ish, one cool — rather than by using
   three unrelated hues. They read as a family because they are one.

   ⚠ Every stop resolves at PAINT time, so a colour-mix that fails leaves a
   transparent card rather than a wrong one. Each gradient therefore ends in a
   flat linear-gradient of the same family, which is the layer that guarantees
   there is always something behind the white type. */
.mesh-1 {
  background:
    radial-gradient(80% 70% at 12% 8%,  color-mix(in srgb, var(--brand) 72%, #f2e9c8) 0%, transparent 60%),
    radial-gradient(70% 70% at 92% 22%, color-mix(in srgb, var(--brand) 60%, #2f3a24) 0%, transparent 62%),
    radial-gradient(90% 80% at 70% 96%, color-mix(in srgb, var(--brand) 25%, #2a3320) 0%, transparent 66%),
    linear-gradient(150deg,
      color-mix(in srgb, var(--brand) 55%, #46512c),
      color-mix(in srgb, var(--brand) 18%, #2b331f));
}
.mesh-2 {
  background:
    radial-gradient(80% 70% at 10% 10%, color-mix(in srgb, var(--brand) 66%, #f0d9cd) 0%, transparent 60%),
    radial-gradient(70% 70% at 90% 25%, color-mix(in srgb, var(--brand) 52%, #4a2f28) 0%, transparent 62%),
    radial-gradient(90% 80% at 65% 95%, color-mix(in srgb, var(--brand) 22%, #3a2620) 0%, transparent 66%),
    linear-gradient(150deg,
      color-mix(in srgb, var(--brand) 48%, #6b4a40),
      color-mix(in srgb, var(--brand) 14%, #3b2b26));
}
.mesh-3 {
  background:
    radial-gradient(80% 70% at 12% 10%, color-mix(in srgb, var(--brand) 40%, #dbe4ec) 0%, transparent 60%),
    radial-gradient(70% 70% at 90% 24%, color-mix(in srgb, var(--brand) 30%, #35424f) 0%, transparent 62%),
    radial-gradient(90% 80% at 68% 95%, color-mix(in srgb, var(--brand) 14%, #2b343e) 0%, transparent 66%),
    linear-gradient(150deg,
      color-mix(in srgb, var(--brand) 26%, #4d5b6a),
      color-mix(in srgb, var(--brand) 8%, #2e3841));
}
.mesh .mesh-top { display: flex; align-items: center; gap: 10px; }
.mesh .mesh-mark {
  width: 34px; height: 34px; border-radius: 50%; flex: none;
  border: 1px solid rgba(255,255,255,.45);
  display: grid; place-items: center;
}
.mesh .mesh-mark svg { width: 16px; height: 16px; }
.mesh .mesh-who { flex: 1; font-size: 14px; font-weight: 500; line-height: 1.25;
                  overflow: hidden; text-overflow: ellipsis; }
/* ⚠ Same three facts mesh-foot already prints in words, a second way - see
   the comment above this markup in dashboard.php. Bright against the mesh
   gradient rather than the status tokens screens.css uses elsewhere: this
   sits on a coloured background, not a white card, and --good/--bad read as
   near-invisible there. */
.mesh-status { position: relative; display: inline-flex; width: 8px; height: 8px; flex: none; }
.mesh-status-core { position: absolute; inset: 0; border-radius: 50%; background: rgba(255,255,255,.4); }
.mesh-status-live .mesh-status-core { background: #4ADE80; box-shadow: 0 0 0 2px rgba(74,222,128,.28); }
.mesh-status-bad  .mesh-status-core { background: #FBBF24; box-shadow: 0 0 0 2px rgba(251,191,36,.28); }
.mesh-status-ring {
  position: absolute; inset: 0; border-radius: 50%; background: #4ADE80;
  animation: mesh-status-pulse 2.4s ease-out infinite;
}
@keyframes mesh-status-pulse {
  0%   { transform: scale(1);   opacity: .55; }
  70%  { transform: scale(2.8); opacity: 0; }
  100% { transform: scale(2.8); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .mesh-status-ring { animation: none; opacity: 0; }
}
.mesh .mesh-lbl { font-size: 12px; color: rgba(255,255,255,.72); }
.mesh .mesh-n   { font-size: 30px; font-weight: 600; letter-spacing: -.03em;
                  line-height: 1; font-variant-numeric: tabular-nums; }
.mesh .mesh-n small { font-size: 15px; font-weight: 500; opacity: .75; }
.mesh .mesh-foot {
  margin-top: auto; display: flex; align-items: flex-end;
  justify-content: space-between; gap: 12px;
  font-size: 12px; color: rgba(255,255,255,.72);
}
/* The circular action, top right, as in every card in the reference. */
.mesh .go {
  position: absolute; top: 16px; right: 16px; z-index: 2;
  width: 34px; height: 34px; border-radius: 50%;
  background: rgba(0,0,0,.55); color: #fff;
  display: grid; place-items: center;
  transition: background .18s ease, transform .18s ease;
}
.mesh .go svg { width: 15px; height: 15px; transform: rotate(-45deg); }
a.mesh:hover .go { background: #000; transform: translate(1px, -1px); }
a.mesh:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
.mesh-row {
  display: grid; gap: 14px; margin: 0 0 20px;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}

/* ── the invitation beside them, not just an empty slot ──────────────────
   ⚠ A DASHED OUTLINE ON ITS OWN READS AS "DISABLED", the same way a greyed-
   out form field does - so this borrows the .mesh cards' own shape (same
   radius, same min-height, same flex column) and answers with warmth
   instead: a soft brand-tinted ring behind the plus, a hover that lifts and
   colours in rather than one that just darkens a border. It is still
   obviously the empty one next to two photographs of real data - that
   contrast is the point - but it should look like an invitation, not a
   placeholder apologising for not being finished yet. */
.mesh-add-wrap { display: contents; }
.mesh-add {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; min-height: 148px; padding: 20px; border-radius: 22px;
  border: 1.5px dashed var(--line-strong, var(--line)); background: var(--surface-2);
  color: var(--ink-2); text-decoration: none; text-align: center; cursor: pointer;
  transition: border-color .18s var(--ease), background .18s var(--ease),
              transform .18s var(--ease), box-shadow .18s var(--ease);
}
/* ⚠ A lit chip, not a flat tinted circle - same radial-mesh technique as
   the primary button's own icon (.btn .cap) and the marketing site's
   matching one, so "add a mailbox" reads as the same kind of invitation a
   primary action is, not a muted placeholder icon. */
.mesh-add-ico {
  width: 42px; height: 42px; border-radius: 50%; display: grid; place-items: center;
  flex: none; color: #fff;
  background:
    radial-gradient(120% 130% at 24% 22%,
      color-mix(in srgb, var(--brand) 55%, #fff) 0%,
      var(--brand) 45%,
      color-mix(in srgb, var(--brand) 78%, var(--ink)) 100%);
  box-shadow: inset 0 1px 1px rgba(255,255,255,.4),
              0 2px 8px -2px color-mix(in srgb, var(--brand) 55%, transparent);
  transition: transform .18s var(--ease);
}
.mesh-add-ico .ico { width: 18px; height: 18px; }
.mesh-add-lbl { font-size: 14px; font-weight: 500; }
.mesh-add:hover, .mesh-add:focus-visible {
  border-color: var(--brand); background: var(--brand-soft); color: var(--brand-ink);
  transform: translateY(-2px); box-shadow: var(--shadow);
}
.mesh-add:hover .mesh-add-ico, .mesh-add:focus-visible .mesh-add-ico { transform: rotate(8deg) scale(1.1); }
/* The one difference between the two: a plan-limited tile still invites the
   click, it just answers a different question once you make it - so the
   icon alone hints that before the popup explains it, rather than the whole
   card reading as a warning. */
.mesh-add.is-limit .mesh-add-ico {
  color: #fff;
  background:
    radial-gradient(120% 130% at 24% 22%,
      color-mix(in srgb, var(--warn) 55%, #fff) 0%,
      var(--warn) 45%,
      color-mix(in srgb, var(--warn) 78%, var(--ink)) 100%);
  box-shadow: inset 0 1px 1px rgba(255,255,255,.4),
              0 2px 8px -2px color-mix(in srgb, var(--warn) 55%, transparent);
}
.mesh-add:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .mesh-add, .mesh-add-ico { transition: none; }
  .mesh-add:hover, .mesh-add:focus-visible { transform: none; }
  .mesh-add:hover .mesh-add-ico, .mesh-add:focus-visible .mesh-add-ico { transform: none; }
}

/* ── the dashboard's AI-connectors widget ─────────────────────────────────
   Three provider marks in a small overlapping stack, the same "who could
   this be" shorthand a handful of avatars in a row already reads as
   elsewhere on the web - beside the heading rather than inside a badge,
   since it is naming three specific assistants rather than standing in for
   one idea the way .page-head-ico's single glyph does. */
.mcp-widget-top { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.mcp-widget-top h2 { margin: 0; }
.mcp-widget-marks { display: flex; flex: none; }
.mcp-widget-marks img {
  width: 30px; height: 30px; border-radius: 50%; object-fit: cover;
  background: var(--surface); border: 2px solid var(--surface);
  box-shadow: 0 0 0 1px var(--line);
}
.mcp-widget-marks img + img { margin-left: -10px; }
.mcp-widget .btn { margin-top: 4px; }

/* ── the light cards beside them ──────────────────────────────────────
   The reference's white tiles: a small round icon, a label, one figure. */
.tiles { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.tile {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 22px; padding: 18px 20px 20px;
  display: flex; flex-direction: column; gap: 12px; min-height: 148px;
  text-decoration: none; color: inherit;
  transition: border-color .18s ease, box-shadow .18s ease, transform .35s var(--bounce, ease);
}
a.tile:hover { border-color: var(--line-strong); box-shadow: var(--shadow-lift);
               transform: translateY(-5px) scale(1.015); }
.tile .tile-top { display: flex; align-items: center; gap: 10px; }
.tile .tile-ico {
  width: 34px; height: 34px; border-radius: 50%; flex: none;
  border: 1px solid var(--line); display: grid; place-items: center;
  color: var(--ink-2);
  transition: transform .35s var(--bounce, ease);
}
a.tile:hover .tile-ico { transform: rotate(-8deg) scale(1.12); }
.tile .tile-ico svg { width: 16px; height: 16px; }
.tile .tile-lbl { font-size: 14px; font-weight: 500; color: var(--ink); }
.tile .tile-n {
  margin-top: auto; font-size: 34px; font-weight: 600; letter-spacing: -.035em;
  line-height: 1; color: var(--ink); font-variant-numeric: tabular-nums;
}
.tile .tile-sub { font-size: 12px; color: var(--ink-3); }
/* The one that wants attention borrows the brand, as .card.need already does. */
.tile.need { border-color: var(--brand-line); background: var(--brand-soft); }
.tile.need .tile-n, .tile.need .tile-lbl { color: var(--brand-ink); }
.tile.need .tile-ico { border-color: var(--brand-line); color: var(--brand-ink); }

@media (max-width: 560px) {
  .mesh-row, .tiles { grid-template-columns: minmax(0, 1fr); }
  .bands { gap: 6px; }
  .band { padding: 12px 10px 24px; }
  .band .lbl { left: 10px; font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
  .tabpills a, .tile, .mesh .go { transition: none; }
  a.tile:hover { transform: none; }
  a.mesh:hover .go { transform: none; }
}

/* ── the spacing utilities the shared views use ───────────────────────
   ⚠ These lived only in console.css, so a shared view that used one was
   spaced on the hosted panel and not on the on-premise admin. Same rule as
   .page-head: a class apps/console/views/ writes has to be dressed by the
   sheet BOTH products load. */
.mt-sm { margin-top: 8px; }
.mt-md { margin-top: 12px; }
.mt-lg { margin-top: 20px; }
.mb-sm { margin-bottom: 8px; }
.mb-md { margin-bottom: 12px; }
.mb-lg { margin-bottom: 20px; }

/* ⚠ .cards IS THE TILE GRID NOW. Several screens wrap stat_card() output in
   `<div class="cards">`, and stat_card emits a tile — so rather than edit each
   of those screens, the old grid name lays out the new component. One name
   fewer to remember, and no screen had to change. */
.cards {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
@media (max-width: 560px) { .cards { grid-template-columns: minmax(0, 1fr); } }

/* ── the language's radius, on the surfaces that are cards ────────────
   ⚠ NOT the --radius token. That one is on inputs, buttons and the tab strip
   as well, and a 20px text field looks like a mistake rather than a style —
   the reference itself uses a large radius on cards and a small one on
   controls. So the softening is applied where a surface is a CARD, and every
   control keeps the radius it had. */
.card, .panel, .queue, .status, .tablewrap, .empty {
  border-radius: 20px;
}
/* The queue's rows sit inside it, so the first and last have to follow the
   corner or the ground shows through as two square notches. */
.queue .q:first-child { border-top-left-radius: 20px; border-top-right-radius: 20px; }
.queue .q:last-child  { border-bottom-left-radius: 20px; border-bottom-right-radius: 20px; }

/* ── the two lists on nearly every screen, arriving rather than appearing ──
   Every screen here is a full page load, so this plays once on paint - no
   observer needed the way the marketing site needs one for content below
   the fold. `backwards` fill-mode holds each row at opacity 0 for its own
   delay rather than flashing visible and then waiting. Capped at ten explicit
   steps: a queue of forty rows staggering individually would still be
   arriving a second and a half after the page painted, which is not quick
   feedback any more, it is a light show. Row 11 onward shares row 10's
   delay, so nothing past that point is waiting on the animation at all. */
.cards > *, .queue > .q, .mesh-row > *, .hero-facts > .f { animation: list-in .45s var(--bounce, cubic-bezier(.34,1.56,.64,1)) backwards; }
@keyframes list-in { from { opacity: 0; transform: translateY(14px) scale(.96); } to { opacity: 1; transform: none; } }
.cards > *:nth-child(1),  .queue > .q:nth-child(1),  .mesh-row > *:nth-child(1), .hero-facts > .f:nth-child(1) { animation-delay: .02s; }
.cards > *:nth-child(2),  .queue > .q:nth-child(2),  .mesh-row > *:nth-child(2), .hero-facts > .f:nth-child(2) { animation-delay: .05s; }
.cards > *:nth-child(3),  .queue > .q:nth-child(3),  .mesh-row > *:nth-child(3), .hero-facts > .f:nth-child(3) { animation-delay: .08s; }
.cards > *:nth-child(4),  .queue > .q:nth-child(4),  .hero-facts > .f:nth-child(4) { animation-delay: .11s; }
.cards > *:nth-child(5),  .queue > .q:nth-child(5)  { animation-delay: .14s; }
.cards > *:nth-child(6),  .queue > .q:nth-child(6)  { animation-delay: .17s; }
.cards > *:nth-child(7),  .queue > .q:nth-child(7)  { animation-delay: .20s; }
.cards > *:nth-child(8),  .queue > .q:nth-child(8)  { animation-delay: .23s; }
.cards > *:nth-child(9),  .queue > .q:nth-child(9)  { animation-delay: .26s; }
.cards > *:nth-child(n+10), .queue > .q:nth-child(n+10) { animation-delay: .29s; }
/* .mesh's own entrance transform must not collide with list-in's: the tile
   carries hover motion on a CHILD (.go), never on itself, so this is safe. */
@media (prefers-reduced-motion: reduce) {
  .cards > *, .queue > .q, .mesh-row > *, .hero-facts > .f { animation: none; }
}

/* ── the dismiss cross ────────────────────────────────────────────────
   Quiet until it is wanted: a panel you can put away should not spend its
   whole life advertising the fact. It reaches full contrast on hover and on
   keyboard focus, and it is a real button, so it is tabbable either way. */
.dismiss { margin: 0; flex: none; line-height: 0; }
.dismiss button {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0;
  border: 0; border-radius: 8px; background: transparent; cursor: pointer;
  color: currentColor; opacity: .45;
  transition: opacity .15s ease, background .15s ease;
}
.dismiss button:hover { opacity: 1; background: rgba(15, 23, 42, .07); }
.dismiss button:focus-visible { opacity: 1; outline: 2px solid currentColor; outline-offset: 1px; }
.dismiss svg { width: 15px; height: 15px; }

/* A flash that carries one: the text takes the room, the cross sits at the end. */
.flash.has-dismiss { display: flex; align-items: flex-start; gap: 12px; }
.flash.has-dismiss > :first-child { flex: 1 1 auto; min-width: 0; }

/* Save, with nothing to save. Not hidden — an operator has to be able to see
   that the control exists and that it is simply not needed yet. */
button.is-clean, button[disabled] { opacity: .45; cursor: default; }
button.is-clean:hover, button[disabled]:hover { transform: none; box-shadow: none; }
