/* dash.css — the app shell for /dash (server dashboard) and /admin (operator console).
   Layout only. Primitives (.btn .chip .field .sw .toast .empty .skel .callout .kv)
   come from ui.css; every value comes from theme.css. Zero literals
   (design/scripts/lint_hardcodes.py). Replaces ngbot.css. */
/* Breakpoints: docs/BRAND.md section 5. Layout tokens sm/md/lg/xl plus menu and
   drawer; queries are written in rem because var() does not work in @media,
   and a max-width query is the breakpoint less 0.0625rem. */
/* The shell owns the viewport: the sidebar is fixed and the main column
   scrolls inside it, so the console fills the screen instead of ending in a
   band of page background on a wide monitor. min-height is reset because
   ui.css gives body min-height:100vh, and on iPhone Safari 100vh is the
   LARGE viewport (836 against 796 visible on a 17 Pro Max): the min-height
   won over this height, and the bottom of every view, the Save bar
   included, sat under Safari's toolbar. */
body{display:flex;height:100dvh;min-height:0;overflow:hidden}

/* ── Sidebar ──────────────────────────────────────────────────────────── */
/* Colour lives with the shell block further down, not here: this rule had a
   background and a border that the later one has overridden since the shell
   was rebuilt, which left two answers to "what colour is the sidebar". */
.side{width:var(--size-sidebar);flex:none;padding:var(--space-5) var(--space-3);
  display:flex;flex-direction:column;gap:var(--space-1);height:100dvh;position:sticky;top:0;overflow-y:auto}
/* gap:0 for the same reason as the login brand: a gap on this flex row falls
   between "ngbot" and ".ai" as well as after the icon, splitting the wordmark. */
.side .brand{display:flex;align-items:center;gap:0;font-family:var(--font-display);
  font-weight:var(--weight-bold);font-size:var(--text-lg);color:var(--color-text-primary);
  padding:var(--space-1) var(--space-2);margin-bottom:var(--space-2);direction:ltr}
.side .brand img{width:var(--size-icon-xl);height:var(--size-icon-xl);
  margin-inline-end:var(--space-gap-sm)}
.brand .tld{color:var(--color-text-brand)}

.server{display:flex;align-items:center;gap:var(--space-gap-sm);padding:var(--space-2);
  background:var(--color-surface-raised);border:1px solid var(--color-border-default);border-radius:var(--radius-lg);
  margin-bottom:var(--space-3);width:100%;text-align:start;transition:border-color var(--duration-fast)}
.server:hover{border-color:var(--color-border-strong)}
.server.static{cursor:default}
.server-icon{width:var(--size-avatar-lg);height:var(--size-avatar-lg);border-radius:var(--radius-icon-well);
  background:var(--gradient-brand);color:var(--color-text-on-action);display:flex;align-items:center;justify-content:center;
  font-weight:var(--weight-bold);font-family:var(--font-display);font-size:var(--text-sm);flex:none}
.server-name{flex:1;min-width:0;overflow:hidden}
.server-name b{display:block;color:var(--color-text-primary);font-size:var(--text-sm);font-weight:var(--weight-semibold);
  white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
.server-name span{font-size:var(--text-xs);color:var(--color-text-tertiary);display:flex;align-items:center;gap:var(--space-1)}
/* The dot used to be painted green unconditionally, beside text that
   sometimes read "degraded". It follows the status now. */
.server-name span.live::before{content:"";width:var(--space-2);height:var(--space-2);
  background:var(--color-feedback-success);border-radius:var(--radius-full);
  box-shadow:0 0 0 var(--width-focus) var(--color-feedback-success-bg);flex:none}
.server-name span.live.warn::before{background:var(--color-feedback-warning);
  box-shadow:0 0 0 var(--width-focus) var(--color-feedback-warning-bg)}
.server-name span.live.bad::before{background:var(--color-feedback-error);
  box-shadow:0 0 0 var(--width-focus) var(--color-feedback-error-bg)}
.server .caret{color:var(--color-text-tertiary)}

.side-section{font-size:var(--text-xs);color:var(--color-text-tertiary);text-transform:uppercase;
  letter-spacing:var(--tracking-wide);font-weight:var(--weight-semibold);padding:var(--space-3) var(--space-3) var(--space-1)}
.nav-item{display:flex;align-items:center;gap:var(--space-gap-md);min-height:var(--size-control-md);padding:0 var(--space-3);
  border-radius:var(--radius-button);color:var(--color-nav-item-text);font-size:var(--text-sm);font-weight:var(--weight-medium);
  width:100%;text-align:start;border:1px solid transparent;
  transition:background-color var(--duration-fast),color var(--duration-fast)}
.nav-item:hover{background:var(--color-nav-item-hover-bg);color:var(--color-text-primary)}
/* A press answers the click before the view changes. */
.nav-item:active,.seg button:active{background:var(--color-interactive-active-overlay)}
.nav-item.active{background:var(--color-nav-item-active-bg);color:var(--color-nav-item-active-text);border-color:var(--color-nav-item-active-border)}
.nav-item.active .icon{color:var(--color-text-brand)}
.nav-item .badge{margin-inline-start:auto;background:var(--gradient-brand);color:var(--color-text-on-action);
  font-size:var(--text-xs);font-weight:var(--weight-bold);padding:0 var(--space-1);border-radius:var(--radius-sm);line-height:var(--leading-normal)}
.nav-item .count{margin-inline-start:auto;font-size:var(--text-xs);color:var(--color-text-tertiary);font-family:var(--font-mono)}
.side-close{display:none}
.side-nav{display:flex;flex-direction:column;gap:var(--space-1)}
.side-foot{margin-top:auto;padding-top:var(--space-3);border-top:1px solid var(--color-border-default)}
.side-foot .nav-item + .nav-item{margin-top:var(--space-1)}
/* The plan card (#upsell) is styled with the other states in "v2 · States". */

/* Loading: three settling lines and a label, replacing the character that used
   to stand in for a skeleton. */
.load{padding:var(--space-6) var(--space-4);display:flex;flex-direction:column;
  gap:var(--space-gap-sm);align-items:stretch}
.load .skel:nth-child(2){inline-size:82%}
.load .skel:nth-child(3){inline-size:58%}
.load-t{font-size:var(--text-xs);color:var(--color-text-tertiary);margin-top:var(--space-1)}

/* ── Main + topbar ────────────────────────────────────────────────────── */
.main{flex:1;min-width:0;padding:var(--space-6) var(--space-8);overflow-y:auto;overflow-x:hidden}
/* Past this width tables ran to thousands of pixels and a label sat a screen away
   from its value; the content keeps a scannable width. It keeps to the start
   edge, beside the sidebar: centred, a wide monitor put a band of empty page
   between the navigation and the thing it navigates. */
.main > .topbar,.main > .view{max-inline-size:var(--size-console-max);margin-inline:0 auto}
.topbar{display:flex;align-items:center;justify-content:space-between;gap:var(--space-4);margin-bottom:var(--space-gap-xl);flex-wrap:wrap}
.topbar-lead{display:flex;align-items:center;gap:var(--space-gap-md)}
.crumbs{color:var(--color-text-tertiary);font-size:var(--text-xs);font-family:var(--font-sans)}
/* The view name is the page's heading, so it is an h1 now. It keeps the
   crumb's look; only the outline changes, which is what screen readers
   navigate by and both consoles were missing entirely. */
.crumbs b,.crumbs h1{color:var(--color-text-primary);font-family:var(--font-display);font-weight:var(--weight-semibold);
  font-size:var(--text-2xl);letter-spacing:var(--tracking-snug);display:block;line-height:var(--leading-snug)}
.topbar-actions{display:flex;gap:var(--space-gap-sm);align-items:center;flex-wrap:wrap}
.menu-btn{display:none}

.search{background:var(--color-input-bg);border:1px solid var(--color-input-border);border-radius:var(--radius-input);
  min-height:var(--size-control-md);padding:0 var(--space-3);display:flex;align-items:center;gap:var(--space-gap-sm);
  font-size:var(--text-sm);color:var(--color-text-tertiary);min-width:calc(var(--size-sidebar) + var(--space-5));
  transition:border-color var(--duration-fast)}
.search:focus-within{border-color:var(--color-input-border-focus)}
.search .icon{width:var(--size-icon-sm);height:var(--size-icon-sm)}
.search input{background:transparent;border:0;color:var(--color-text-primary);outline:none;font-size:var(--text-sm);flex:1;min-width:0}
/* outline:none above beats the global :focus-visible ring on specificity, so
   the ring has to be handed back. It goes on the wrapper, which is the thing
   that reads as the field. */
.search:has(input:focus-visible){outline:var(--width-focus) solid var(--color-border-focus);
  outline-offset:var(--width-focus)}
.search input::placeholder{color:var(--color-input-placeholder)}
.search kbd{background:var(--color-surface-raised);border:1px solid var(--color-border-default);color:var(--color-text-tertiary);
  font-size:var(--text-xs);padding:0 var(--space-1);border-radius:var(--radius-sm)}
.panel-head .search{min-width:0;flex:1;max-width:calc(var(--size-sidebar) - var(--space-5))}

/* ── Stat tiles ───────────────────────────────────────────────────────── */
.stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--space-gap-lg);margin-bottom:var(--space-gap-lg)}
.stat{background:var(--color-card-bg);border:1px solid var(--color-card-border);border-radius:var(--radius-card);padding:var(--space-5)}
/* Four tiles, so four columns or two: auto-fit left a 3 + 1 orphan row between
   about 976 and 1190 wide. The narrower rules below still win. */
@media (max-width:75rem){.stats{grid-template-columns:repeat(2,minmax(0,1fr))}}
/* A tile that drills is a <button>, so it has to be told to stop looking like
   one: full width, inherited type, text starting where the rest of the tile's
   text starts. .drill supplies the hover and the focus ring. */
button.stat{width:100%;font:inherit;text-align:start;cursor:pointer;display:flex;flex-direction:column}
/* A button centres its content when the grid stretches it to the tallest tile,
   so the drill tiles are a column like the usage tile, and the sub-line sits
   at the foot of every tile in the row. */
.stats .stat{display:flex;flex-direction:column}
#billingBody > .callout{margin-bottom:var(--space-gap-lg)}
.stats .stat .delta{margin-block-start:auto}
.stat.usage .head{margin-bottom:0}
/* Labels wrap to two lines on narrow tiles; reserve the taller of the two so
   every value sits on the same baseline across the row. */
.stat .head{display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-gap-sm);
  margin-bottom:var(--space-gap-sm);min-height:var(--size-control-sm);color:var(--color-text-tertiary);
  font-size:var(--text-xs);text-transform:uppercase;letter-spacing:var(--tracking-wide);font-weight:var(--weight-semibold);line-height:var(--leading-snug)}
.stat .head .ic{width:var(--size-control-sm);height:var(--size-control-sm);background:var(--gradient-brand-soft);
  border-radius:var(--radius-md);display:flex;align-items:center;justify-content:center;color:var(--color-text-brand);flex:none}
.stat .head .ic .icon{width:var(--size-icon-sm);height:var(--size-icon-sm)}
.stat .val{font-family:var(--font-display);font-weight:var(--weight-bold);font-size:var(--text-3xl);color:var(--color-text-primary);
  letter-spacing:var(--tracking-snug);line-height:var(--leading-none);font-variant-numeric:tabular-nums}
.stat .val small{font-family:var(--font-mono);font-size:var(--text-sm);color:var(--color-text-tertiary);font-weight:var(--weight-regular)}
.stat .delta{font-size:var(--text-xs);color:var(--color-feedback-success);margin-top:var(--space-1);font-family:var(--font-mono);
  display:inline-flex;align-items:center;gap:var(--space-1);
  /* An arrow and a percentage are LTR fragments. Without isolation the
     leading glyph jumps to the wrong end of an Arabic line. */
  unicode-bidi:isolate}
.stat .delta.dn{color:var(--color-feedback-error)}
.stat .delta.flat{color:var(--color-text-tertiary)}
/* Over quota is not a neutral fact. Without this a server at three times its
   allowance looked exactly like one at forty percent. */
.stat .delta.over{color:var(--color-chart-warn)}
.stat.over .val small{color:var(--color-chart-warn)}

/* ── Panels ───────────────────────────────────────────────────────────── */
.row{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr);gap:var(--space-gap-lg)}
.row.split{grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:start}
.panel{background:var(--color-card-bg);border:1px solid var(--color-card-border);border-radius:var(--radius-panel);padding:var(--space-card)}
.panel + .panel,.row + .panel,.stats + .panel,
/* .row followed by .wg had no rule at all, so the two panel hairlines
   abutted and read as one box. */
.panel + .wg,.row + .wg,.stats + .wg{margin-top:var(--space-gap-lg)}
.panel-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-gap-md);gap:var(--space-gap-md);flex-wrap:wrap}
.panel-head h2,.panel-head h3{font-family:var(--font-display);font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--color-text-primary);
  line-height:var(--size-control-xs)}
.link{font-size:var(--text-xs);color:var(--color-text-link);font-weight:var(--weight-medium)}
.link:hover{color:var(--color-text-link-hover)}
.seg{display:flex;gap:var(--space-1);flex-wrap:wrap}
.seg button{font-size:var(--text-xs);color:var(--color-text-tertiary);min-height:var(--size-control-xs);padding:0 var(--space-2);
  border-radius:var(--radius-chip);border:1px solid transparent;font-weight:var(--weight-medium);
  transition:background-color var(--duration-fast),color var(--duration-fast)}
.seg button:hover{color:var(--color-text-primary);background:var(--color-interactive-hover-overlay)}
.seg button.active{color:var(--color-text-primary);background:var(--color-interactive-selected-bg);border-color:var(--color-interactive-selected-border-strong)}
.actions{margin-top:var(--space-gap-lg);display:flex;gap:var(--space-gap-sm);flex-wrap:wrap}
.note{margin-top:var(--space-gap-md);font-size:var(--text-sm)}

/* ── Chart (drawn by JS with classes, coloured here) ──────────────────── */
/* No margin-top: .panel-head already owns the gap below the heading, and
   in a flex column this one adds to it instead of collapsing. */
/* A flex column so the component measures everything it contains. The plot had
   `height:100%` inside a `min-height` box, so the axis and the caption appended
   after it were laid out past the panel's edge and appeared to float in the gap
   above the next card. The plot now has its own height and the labels below it
   are ordinary flow children. */
.chart{position:relative;display:flex;flex-direction:column;gap:var(--space-gap-sm)}
.chart-plot{position:relative;flex:none;direction:ltr}
.chart svg{width:100%;height:11.25rem;display:block;overflow:visible}
.chart .grid{stroke:var(--color-chart-grid)}
.chart .area-stop{color:var(--color-chart-series-1)}
.chart .line{stroke:var(--color-chart-series-1);stroke-width:var(--width-focus)}
.chart .axis{display:flex;justify-content:space-between;font-family:var(--font-mono);font-size:var(--text-xs);
  color:var(--color-text-tertiary);margin-top:var(--space-gap-xs)}

/* ── Activity feed + avatars ─────────────────────────────────────────── */
.act{display:flex;align-items:center;gap:var(--space-gap-md);padding:var(--space-2) 0;border-top:1px solid var(--color-border-default);font-size:var(--text-sm)}
.act:first-of-type{border-top:0}
.av,.lb-av{width:var(--size-avatar-md);height:var(--size-avatar-md);border-radius:var(--radius-avatar);flex:none;
  display:flex;align-items:center;justify-content:center;font-weight:var(--weight-bold);font-family:var(--font-display);
  font-size:var(--text-xs);background:var(--color-avatar-bg);color:var(--color-avatar-text)}
.lb-av.sm{width:var(--size-avatar-sm);height:var(--size-avatar-sm)}
/* Per-user hue from a hash of the id: five chart series, ink on top (≥7:1 on all five) */
[data-hue]{color:var(--color-text-on-action)}
[data-hue="1"]{background:var(--color-chart-series-1)}
[data-hue="2"]{background:var(--color-chart-series-2)}
[data-hue="3"]{background:var(--color-chart-series-3)}
[data-hue="4"]{background:var(--color-chart-series-4)}
[data-hue="5"]{background:var(--color-chart-series-5)}
.act .body{flex:1;min-width:0;color:var(--color-text-secondary);overflow-wrap:anywhere}
.act .body b{color:var(--color-text-primary)}
.act .ts{color:var(--color-text-tertiary);font-family:var(--font-mono);font-size:var(--text-xs);flex:none}
.cell{display:inline-flex;align-items:center;gap:var(--space-gap-sm)}

/* ── Module toggles ───────────────────────────────────────────────────── */
.modules{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--space-gap-lg);margin-top:var(--space-gap-lg)}
.modules > .mod:last-child:nth-child(odd){grid-column:1 / -1}
.mod{background:var(--color-card-bg);border:1px solid var(--color-card-border);border-radius:var(--radius-lg);padding:var(--space-4);
  display:flex;align-items:center;gap:var(--space-gap-md)}
.mod .ic{width:var(--size-icon-well);height:var(--size-icon-well);background:var(--gradient-brand-soft);
  border:1px solid var(--color-interactive-selected-border);border-radius:var(--radius-icon-well);
  display:flex;align-items:center;justify-content:center;color:var(--color-text-brand);flex:none}
.mod .txt{flex:1;min-width:0}
.mod b{color:var(--color-text-primary);font-family:var(--font-display);font-size:var(--text-sm);display:block}
.mod span{font-size:var(--text-xs);color:var(--color-text-tertiary);display:block;margin-top:var(--space-1)}

/* ── Leaderboard ──────────────────────────────────────────────────────── */
.lb-row{display:flex;align-items:center;gap:var(--space-gap-md);padding:var(--space-2) 0;border-top:1px solid var(--color-border-default)}
.lb-row:first-of-type{border-top:0}
.lb-rank{width:var(--size-icon-lg);text-align:center;font-family:var(--font-display);font-weight:var(--weight-bold);color:var(--color-text-tertiary);font-size:var(--text-xs)}
.lb-rank.auto{width:auto}
.lb-rank.gold{color:var(--color-brand-gold)}
.lb-rank.silver{color:var(--color-text-secondary)}
.lb-rank.bronze{color:var(--color-brand-ember)}
.lb-name{flex:1;min-width:0;color:var(--color-text-primary);font-size:var(--text-sm);font-weight:var(--weight-medium);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lb-lvl{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text-tertiary)}
.lb-xp,.num.brand{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text-brand);font-weight:var(--weight-semibold)}

/* ── Tables ───────────────────────────────────────────────────────────── */
/* position:relative so a .sr-only header (absolute) scrolls with the table
   instead of escaping to the page: an action column past 390 widened the
   whole document on phones. */
.tbl-wrap{overflow-x:auto;position:relative}
/* Scroll cue: covers ride with the content (local), edge glows stay at the
   edges (scroll), so a glow shows only on a side that has hidden columns.
   A dark shadow does not read on a dark card; a light edge does. */
.tbl-wrap{background:
  linear-gradient(to right,var(--color-card-bg) 30%,transparent) left center/var(--space-10) 100% no-repeat local,
  linear-gradient(to left,var(--color-card-bg) 30%,transparent) right center/var(--space-10) 100% no-repeat local,
  radial-gradient(farthest-side at 0 50%,var(--color-border-strong),transparent) left center/var(--space-2) 100% no-repeat scroll,
  radial-gradient(farthest-side at 100% 50%,var(--color-border-strong),transparent) right center/var(--space-2) 100% no-repeat scroll}
/* A percentage-wide control adds nothing to an auto-layout column's
   min-content, so the ticket status select was squeezed to one letter. */
.tbl .input{width:auto;min-inline-size:max-content}
/* Knowledge preview: clamped inside the cell (a td must stay table-cell).
   Below 40rem the column goes and the clamp moves under the source name. */
.kb-prev{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;max-inline-size:var(--size-prose-max)}
.kb-prev-in{display:none}
@media (max-width:40rem){
  .tbl .col-preview,.tbl .col-cat{display:none}
  .kb-prev-in{display:-webkit-box;margin-top:var(--space-1);color:var(--color-text-tertiary);font-size:var(--text-xs)}
}
.tbl{width:100%;border-collapse:collapse;font-size:var(--text-sm)}
.tbl th{text-align:start;color:var(--color-table-header-text);font-size:var(--text-xs);text-transform:uppercase;
  letter-spacing:var(--tracking-wide);font-weight:var(--weight-semibold);padding:var(--space-2);
  border-bottom:1px solid var(--color-table-border);white-space:nowrap}
.tbl td{padding:var(--space-3) var(--space-2);border-bottom:1px solid var(--color-table-border);color:var(--color-text-secondary);vertical-align:middle}
.tbl tbody tr{transition:background-color var(--duration-fast)}
.tbl tbody tr:hover{background:var(--color-table-row-hover)}
.tbl tbody tr.clickable{cursor:pointer}
/* A filter you switched on has to look switched on, or the shortened
   table below it reads as missing data. */
.tbl tbody tr.clickable.on{background:var(--color-table-row-hover);
  box-shadow:inset var(--width-focus) 0 0 0 var(--color-brand-ember)}
/* box-shadow has no logical form, so the rail needs mirroring by hand or it
   lands at the row's end in the RTL console, opposite the cell it marks. */
[dir=rtl] .tbl tbody tr.clickable.on{
  box-shadow:inset calc(var(--width-focus) * -1) 0 0 0 var(--color-brand-ember)}
.tbl tbody tr.clickable.on td:first-child{color:var(--color-text-primary);
  font-weight:var(--weight-semibold)}
.tbl td b{color:var(--color-text-primary);font-weight:var(--weight-semibold)}
.tbl .num{font-family:var(--font-mono);font-size:var(--text-xs)}
.tbl .sub{color:var(--color-text-tertiary);font-size:var(--text-xs)}
/* Phone: the ticket and moderation tables become one card per row, each cell
   labelled with its column (dash_ui.html tableView, cfg.stack). A long subject
   used to be squeezed into a narrow column (parked item, 2026-09-25). */
@media (max-width:40rem){
  .tbl.stack,.tbl.stack tbody,.tbl.stack tr{display:block;inline-size:100%}
  .tbl.stack thead{position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
  .tbl.stack tr{padding:var(--space-3) var(--space-2);border-bottom:1px solid var(--color-table-border)}
  .tbl.stack td,.tbl.stack td.col-cat,.tbl.stack td.col-preview{display:flex;gap:var(--space-3);align-items:baseline;
    padding:var(--space-1) 0;border-bottom:0;min-inline-size:0}
  .tbl.stack td[data-label]::before{content:attr(data-label);flex:0 0 var(--space-24);color:var(--color-table-header-text);
    font-size:var(--text-xs);font-weight:var(--weight-semibold)}
  .tbl.stack td.row-end{padding-top:var(--space-2)}
}

/* ── Pagination ───────────────────────────────────────────────────────── */
.pager{display:flex;align-items:center;justify-content:space-between;gap:var(--space-gap-md);margin-top:var(--space-gap-md);flex-wrap:wrap}
.pager .info{font-size:var(--text-xs);color:var(--color-text-tertiary);font-family:var(--font-mono)}
.pager .ctrls{display:flex;align-items:center;gap:var(--space-1);flex-wrap:wrap}
.pager button{min-width:var(--size-control-sm);height:var(--size-control-sm);padding:0 var(--space-2);border-radius:var(--radius-md);
  border:1px solid var(--color-border-default);background:var(--color-surface-raised);color:var(--color-text-secondary);
  font-size:var(--text-xs);font-weight:var(--weight-semibold);transition:background-color var(--duration-fast),color var(--duration-fast)}
.pager button:hover:not(:disabled){background:var(--color-action-secondary-hover);color:var(--color-text-primary)}
.pager button:disabled{opacity:var(--opacity-disabled);cursor:not-allowed}
.pager button.current{background:var(--gradient-brand);color:var(--color-text-on-action);border-color:transparent}

/* ── Drawer ───────────────────────────────────────────────────────────── */
.drawer{position:fixed;top:0;inset-inline-end:0;width:var(--size-drawer);height:100dvh;background:var(--color-surface-card);
  border-inline-start:1px solid var(--color-border-default);box-shadow:var(--shadow-lg);z-index:var(--z-drawer);
  overflow-y:auto;padding:var(--space-6)}
.drawer h3{margin-bottom:var(--space-1);padding-inline-end:var(--size-control-md)}
/* sticky, not absolute: the drawer scrolls its own content, and an
   absolute close button scrolled away with it. Escape and the scrim still
   work, but the visible affordance has to stay visible. */
.drawer .close{position:sticky;top:0;float:inline-end;margin-inline-end:calc(var(--space-4) * -1);width:var(--size-control-sm);height:var(--size-control-sm);
  display:grid;place-items:center;border-radius:var(--radius-md);color:var(--color-text-tertiary);
  transition:background-color var(--duration-fast),color var(--duration-fast)}
.drawer .close:hover{background:var(--color-interactive-hover-overlay);color:var(--color-text-primary)}
.drawer .meta{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text-tertiary)}
.drawer .pre{white-space:pre-wrap}
.drawer h3,.drawer .meta,.drawer .pre{overflow-wrap:anywhere}
.drawer{overflow-x:hidden}
.drawer section{margin-top:var(--space-gap-xl)}
.drawer section>h4{font-size:var(--text-xs);text-transform:uppercase;letter-spacing:var(--tracking-wide);color:var(--color-text-tertiary);
  font-family:var(--font-sans);font-weight:var(--weight-semibold);margin-bottom:var(--space-gap-sm)}
.check{display:flex;gap:var(--space-gap-sm);align-items:center;font-size:var(--text-sm);margin-bottom:var(--space-gap-lg);cursor:pointer;color:var(--color-text-secondary)}
.check input{width:var(--size-icon-md);height:var(--size-icon-md);accent-color:var(--color-action-primary)}

/* ── Operator console extras ──────────────────────────────────────────── */
.tierbar{display:flex;height:var(--space-2);border-radius:var(--radius-full);overflow:hidden;background:var(--color-surface-raised)}
.tierbar i{display:block;height:100%}
.legend{display:flex;gap:var(--space-4);flex-wrap:wrap;margin-top:var(--space-gap-md);font-size:var(--text-xs)}
.legend span{display:inline-flex;align-items:center;gap:var(--space-gap-sm);color:var(--color-text-secondary)}
.legend i{width:var(--space-2);height:var(--space-2);border-radius:var(--radius-sm);display:inline-block}
.tier-starter{background:var(--color-chart-series-5)}
.tier-pro{background:var(--color-chart-series-1)}
.tier-enterprise{background:var(--color-chart-series-2)}

/* ── Views ────────────────────────────────────────────────────────────── */
.view{display:none}
.view.active{display:block}

/* ── Responsive: off-canvas sidebar below 900px (there was no nav at all) ── */
@media (max-width:900px){
  body{flex-direction:column}
  .side{position:fixed;inset-block:0;inset-inline-start:0;height:100dvh;transform:translateX(-100%);
    transition:transform var(--duration-moderate) var(--ease-out);z-index:var(--z-drawer);box-shadow:var(--shadow-lg)}
  [dir=rtl] .side{transform:translateX(100%)}
  .side.open{transform:none}
  .side-close{display:inline-flex;position:absolute;inset-block-start:var(--space-4);inset-inline-end:var(--space-3)}
  .menu-btn{display:inline-flex}
  .main{padding:var(--space-4)}
  .stats{grid-template-columns:repeat(2,1fr)}
  .row,.row.split{grid-template-columns:minmax(0,1fr)}
  .modules{grid-template-columns:1fr}
  .search{min-width:0;flex:1}
  .crumbs b,.crumbs h1{font-size:var(--text-xl)}
  .drawer{width:100vw}
}
@media (max-width:560px){
  .stats{grid-template-columns:1fr}
  .topbar-actions{width:100%}
}

/* ── Login (both consoles share it) ───────────────────────────────────── */
.login{min-height:100dvh;width:100%;display:grid;place-items:center;padding:var(--space-6)}
.login-card{width:min(100%, 24rem);text-align:center;padding:var(--space-8) var(--space-6);
  background:var(--color-card-bg);border:1px solid var(--color-card-border);
  border-radius:var(--radius-panel);box-shadow:var(--shadow-lg)}
.login-card img{width:var(--size-avatar-xl);height:var(--size-avatar-xl);margin:0 auto var(--space-3)}
.login-card h1{font-size:var(--text-2xl);margin-bottom:var(--space-1);direction:ltr}
.login-card .sub{color:var(--color-text-tertiary);font-size:var(--text-sm);margin-bottom:var(--space-gap-xl)}
.login-card .err{min-height:var(--space-5);color:var(--color-feedback-error-text);font-size:var(--text-sm);margin-bottom:var(--space-gap-sm)}
.login-card .field{text-align:start}
.or{display:flex;align-items:center;gap:var(--space-gap-sm);color:var(--color-text-tertiary);font-size:var(--text-xs);
  text-transform:uppercase;letter-spacing:var(--tracking-wide);margin:var(--space-gap-lg) 0}
.or::before,.or::after{content:"";flex:1;height:1px;background:var(--color-border-default)}

/* Language toggle — same rule as the marketing site, so the control looks and
   behaves identically on both surfaces. */
.lang-btn .icon{color:var(--color-text-brand)}
.lang-btn span{min-width:var(--space-5);text-align:center;font-family:inherit;font-size:var(--text-xs)}
@media (max-width:640px){.lang-btn span{display:none}}

/* Secondary text in a panel header: what a table is ordered by, what window it
   covers, why it is empty. `.meta` already existed but is scoped to `.drawer`,
   and a class the stylesheet never sees renders as body text pretending to be a
   caption. */
.panel-head .note{font-size:var(--text-xs);color:var(--color-text-tertiary);
  font-weight:var(--weight-regular);text-align:end;max-width:32ch}


/* ═══════════════════════════════════════════════════════════════════════
   Console widgets
   ───────────────────────────────────────────────────────────────────────
   Everything below renders with CSS and inline SVG. No chart library: the
   payloads are small, the shapes are simple, and a 90KB dependency to draw
   sixty line segments is not a trade worth making on a dashboard that has to
   feel instant.

   Series colours come from --color-chart-series-1..8, which are one hue walk
   at ember's own lightness and chroma, so a six-series chart reads as one
   family. Every one clears 7:1 on --color-card-bg; the floor for a graphical
   object is 3:1.
   ═══════════════════════════════════════════════════════════════════════ */

/* stretch, not start: side-by-side panels are a row, and a short one left at
   its content height opens a gap beside a tall one instead of squaring off
   with it. The grid's own gap is the only spacing between them. */
.wg{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));
  gap:var(--space-gap-lg);margin-bottom:var(--space-gap-lg);align-items:stretch}
/* A grid nested in a panel sits on the panel padding, not on a page gap. */
.panel > .wg{margin-bottom:0}
/* .panel + .panel spaces stacked panels in normal flow. Inside a grid the
   second panel is still an adjacent sibling, so it was being pushed down and
   the two panels in a row did not even start level. Applies to every grid
   container that holds panels, not just .wg. */
.wg > .panel + .panel,
.row > .panel + .panel{margin-top:0}
/* A stretched panel has to put the extra height somewhere deliberate rather
   than leave it under the content. */
.wg > .panel,
.row > .panel{display:flex;flex-direction:column}
.wg > .panel > .tbl-wrap, .row > .panel > .tbl-wrap,
.wg > .panel > .chart,    .row > .panel > .chart,
.wg > .panel > .chartbox, .row > .panel > .chartbox{flex:1 1 auto;min-height:0}
.wg > *{grid-column:span 12}
@media (min-width:640px){
  .w3{grid-column:span 6}.w4{grid-column:span 6}.w6{grid-column:span 6}
  .w8{grid-column:span 12}
}
@media (min-width:640px) and (max-width:1099px){#qualityTiles > .w4{grid-column:span 4}}
@media (min-width:1100px){
  .w3{grid-column:span 3}.w4{grid-column:span 4}.w6{grid-column:span 6}
  .w8{grid-column:span 8}
}

/* ── KPI tile ─────────────────────────────────────────────────────────── */
.kpi{background:var(--color-card-bg);border:1px solid var(--color-card-border);
  border-radius:var(--radius-card);padding:var(--space-5);position:relative;
  display:flex;flex-direction:column;gap:var(--space-2);width:100%;text-align:start;
  transition:border-color var(--duration-fast),background-color var(--duration-fast)}
.kpi::before{content:"";position:absolute;inset-inline-start:0;inset-block:var(--space-5);
  width:var(--width-focus);border-radius:var(--radius-full);background:var(--accent,var(--color-text-brand));opacity:0}
.kpi.tone::before{opacity:1}
.kpi-l{display:flex;align-items:center;gap:var(--space-gap-sm);color:var(--color-text-tertiary);
  font-size:var(--text-xs);text-transform:uppercase;letter-spacing:var(--tracking-wide);
  font-weight:var(--weight-semibold);line-height:var(--leading-snug)}
[dir=rtl] .kpi-l{letter-spacing:normal;text-transform:none}
/* Same for every other uppercased, tracked label that receives a translated
   string. Arabic has no capitals, and tracking pulls the joins apart. */
[dir=rtl] .side-section,
[dir=rtl] .stat .head,
[dir=rtl] .tbl th{letter-spacing:normal;text-transform:none}
/* Arabic has no case and joins its letters, so the tracking and uppercase
   these labels and titles carry in English come off in Arabic (C-ctokens-15). */
[dir=rtl] :is(.side-section,.crumbs b,.crumbs h1,.stat .head,.drawer h3,.drawer section>h4,.or){letter-spacing:normal;text-transform:none}
.kpi-l .icon{width:var(--size-icon-sm);height:var(--size-icon-sm);color:var(--accent,var(--color-text-brand));flex:none}
.kpi-v{font-family:var(--font-display);font-weight:var(--weight-bold);font-size:var(--text-3xl);
  color:var(--color-text-primary);letter-spacing:var(--tracking-snug);line-height:var(--leading-none);
  font-variant-numeric:tabular-nums;display:flex;align-items:baseline;gap:var(--space-gap-xs);direction:ltr}
[dir=rtl] .kpi-v{justify-content:flex-end}
.kpi-v small{font-family:var(--font-sans);font-size:var(--text-sm);font-weight:var(--weight-medium);
  color:var(--color-text-tertiary)}
.kpi-f{display:flex;align-items:center;justify-content:space-between;gap:var(--space-gap-sm);margin-top:auto}
.kpi-d{font-size:var(--text-xs);font-family:var(--font-mono);display:inline-flex;align-items:center;
  gap:var(--space-1);color:var(--color-text-tertiary)}
/* The sparkline was width:100% in this flex row, so on a 1280-wide laptop it
   took three quarters of the row and wrapped the label one word per line. The
   label is what you read; the sparkline is what you glance at. */
.kpi-f .kpi-d{flex:1 1 60%;min-width:0}
.kpi-f .spark{flex:0 1 40%;min-width:0}
.kpi-d.up{color:var(--color-chart-ok)}
.kpi-d.dn{color:var(--color-chart-bad)}
/* Direction is not sentiment. A falling error rate is good news, and the tile
   that says so has to be able to paint a downward arrow green. */
.kpi-d.good{color:var(--color-chart-ok)}
.kpi-d.bad{color:var(--color-chart-bad)}
.kpi-d.warn{color:var(--color-chart-warn)}

/* ── Drill-down ───────────────────────────────────────────────────────── */
/* Anything that opens detail says so before it is clicked, and can be reached
   without a mouse. The chevron is the promise; the focus ring is the contract. */
.drill{cursor:pointer}
.drill:hover{border-color:var(--color-card-border-hover);background:var(--color-card-bg-hover)}
.drill:focus-visible{outline:var(--width-focus) solid var(--color-border-focus);outline-offset:var(--width-focus)}

/* ── Sparkline / area / bars ──────────────────────────────────────────── */
.spark{display:block;width:100%;height:var(--space-10);overflow:visible}
.spark path{fill:none;stroke:var(--accent,var(--color-text-brand));stroke-width:var(--width-focus);
  stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke}
.spark .fill{fill:var(--accent,var(--color-text-brand));opacity:var(--opacity-disabled);stroke:none}
/* The console chart's area: a quiet series tint, not the SVG default black. */
.chartbox .fill{fill:var(--accent,var(--color-text-brand));opacity:var(--opacity-selected);stroke:none}
/* The server switcher is a modal: its backdrop sits above the drawer. */
#srvScrim{z-index:var(--z-modal-scrim)}

.chartbox{position:relative;width:100%}
.chartbox svg{display:block;width:100%;height:auto}
/* In a stretched panel the box has a definite height, so the chart takes
   it. Without this the slack opened between the chart and its key. */
.wg > .panel > .chartbox svg,
.row > .panel > .chartbox svg{height:100%}
.chartbox .gridline{stroke:var(--color-chart-grid);stroke-width:var(--width-hairline);vector-effect:non-scaling-stroke}

/* ── Distribution / stacked share bar ─────────────────────────────────── */
.share{display:flex;height:var(--space-2);border-radius:var(--radius-full);overflow:hidden;
  background:var(--color-surface-sunken);margin-block-end:var(--space-3)}
.share i{display:block;height:100%;background:var(--seg,var(--color-text-brand));
  transition:width var(--duration-moderate) var(--ease-out)}
.keys{display:flex;flex-wrap:wrap;gap:var(--space-gap-sm) var(--space-4)}
/* A caption in the key row is a message, not a series: no swatch. */
.keys .cap::before{content:none}
.keys span{display:inline-flex;align-items:center;gap:var(--space-gap-xs);font-size:var(--text-xs);
  color:var(--color-text-secondary)}
.keys span::before{content:"";width:var(--space-2);height:var(--space-2);border-radius:var(--radius-sm);
  background:var(--seg,var(--color-text-brand));flex:none}

/* ── Heartbeat ────────────────────────────────────────────────────────── */
/* A state machine, not decoration. The old dot was painted green
   unconditionally, beside text that sometimes said "degraded".

   Healthy is STATIC. A dot that pulses forever is ambient noise that people
   stop seeing, which means it can no longer signal anything — so motion is
   spent only on the states that want attention. The one exception is the
   single global indicator in the topbar, which earns a slow expanding ring
   because the owner should be able to tell at a glance that the page is live;
   per-tile dots never get it. */
.beat{display:inline-flex;align-items:center;gap:var(--space-gap-xs);font-size:var(--text-xs);
  color:var(--color-text-tertiary);font-family:var(--font-sans)}
.beat::before{content:"";position:relative;width:var(--space-2);height:var(--space-2);
  border-radius:var(--radius-full);background:var(--color-chart-ok);flex:none}
.beat.stale::before{background:var(--color-chart-warn)}
.beat.down::before,.beat.denied::before{background:var(--color-chart-bad)}
.beat.off::before{background:var(--color-text-tertiary)}

/* The global one only: a ring leaving the dot, so "live" is legible without
   the dot itself flickering. */
.beat.pulse::after{content:"";position:absolute;width:var(--space-2);height:var(--space-2);
  border-radius:var(--radius-full);background:var(--color-chart-ok);
  animation:beat-ring calc(var(--duration-slow) * 3.2) var(--ease-out) infinite}
.beat.pulse{position:relative}
@keyframes beat-ring{0%{opacity:.7;transform:scale(1)}70%{opacity:.12;transform:scale(2.6)}
  100%{opacity:0;transform:scale(2.6)}}
@media (prefers-reduced-motion:reduce){
  .beat.pulse::after{animation-duration:calc(var(--duration-ambient) * 1.4)}
}

/* ── Stale data ───────────────────────────────────────────────────────── */
/* A background refetch must never swap real numbers for a skeleton. Only the
   very first load has nothing to show; after that the old value dims and a
   hairline runs along the top of the panel. */
.refreshing{position:relative}
.refreshing::before{content:"";position:absolute;inset-block-start:0;inset-inline:0;
  height:var(--width-focus);border-radius:var(--radius-full);background:var(--gradient-horizon);
  animation:sweep calc(var(--duration-slow) * 2.4) var(--ease-in-out) infinite}
@keyframes sweep{0%{opacity:.25}50%{opacity:1}100%{opacity:.25}}
@media (prefers-reduced-motion:reduce){.refreshing::before{animation:none}}

/* ── Threshold accents ────────────────────────────────────────────────── */
.t-ok{--accent:var(--color-chart-ok)}
.t-warn{--accent:var(--color-chart-warn)}
.t-bad{--accent:var(--color-chart-bad)}
.s1{--accent:var(--color-chart-series-1);--seg:var(--color-chart-series-1)}
.s2{--accent:var(--color-chart-series-2);--seg:var(--color-chart-series-2)}
.s3{--accent:var(--color-chart-series-3);--seg:var(--color-chart-series-3)}
.s4{--accent:var(--color-chart-series-4);--seg:var(--color-chart-series-4)}
.s5{--accent:var(--color-chart-series-5);--seg:var(--color-chart-series-5)}
.s6{--accent:var(--color-chart-series-6);--seg:var(--color-chart-series-6)}
.s7{--accent:var(--color-chart-series-7);--seg:var(--color-chart-series-7)}
.s8{--accent:var(--color-chart-series-8);--seg:var(--color-chart-series-8)}

/* ═══════════════════════════════════════════════════════════════════════
   Console shell v2
   The ops console is used on wide monitors, so it is full-bleed: a content
   cap wastes half a 27" screen. The sidebar takes the SAME background as the
   canvas and is separated by one hairline — tinting it is what makes dark
   consoles look like stacked gray boxes.
   ═══════════════════════════════════════════════════════════════════════ */
/* One step off the canvas rather than the same colour as it. The old
   near-black had nowhere below the page to go, so the sidebar was the
   page plus a hairline; separation is carried by surface now. */
.side{background:var(--color-surface-sidebar);border-inline-end:1px solid var(--color-border-subtle)}
.main.bleed{padding:var(--space-4) var(--space-5)}
.main.bleed .wg{gap:var(--space-3)}

/* Hairlines as rings: a box-shadow never enters the box model, so a hover or
   focus swap cannot shift layout by a pixel. */
.kpi,.panel{box-shadow:0 0 0 var(--width-hairline) var(--color-card-border);border:0}
.kpi:hover.drill{box-shadow:0 0 0 var(--width-hairline) var(--color-card-border-hover)}
/* forced-colors drops box-shadow entirely, taking every hairline with it. */
@media (forced-colors:active){.kpi,.panel{border:var(--width-hairline) solid CanvasText}}

/* Dense mode for the operator console: body at --text-sm, tighter cell padding. The customer
   dashboard keeps the roomier default — different users, different lane. */
.dense{font-size:var(--text-sm)}
.dense .tbl td,.dense .tbl th{padding:var(--space-2) var(--space-3)}
.dense .kpi{padding:var(--space-4);gap:var(--space-1)}
.dense .kpi-v{font-size:var(--text-2xl)}
/* ── Operator console (/admin main is the only .dense) ─────────────────── */
/* Cost leaders rows are <button>s there, and a button shrinks to fit. */
button.lb-row{width:100%;font:inherit;text-align:start;color:inherit}
.dense .lb-row .lb-tier{flex:none}
.dense .lb-row .share{flex:0 0 var(--space-16);margin-block-end:0}
.dense .lb-row .lb-xp{flex:none;white-space:nowrap}
@media (max-width:26rem){
  .dense .lb-row{flex-wrap:wrap}
  .dense .lb-row .lb-name{flex:1 1 60%}
  .dense .lb-row .share{flex:1 1 auto}
}
/* Rows stay rows; the first-cell button is the keyboard path. */
.dense .tbl .rowlink{padding:0;text-align:start;color:inherit;border-radius:var(--radius-sm)}
.dense .tbl .rowlink:focus-visible{outline:var(--width-focus) solid var(--color-border-focus);outline-offset:var(--width-focus)}
/* Long tokens wrap inside the cell; prose cells keep a readable measure.
   break-word, not anywhere: anywhere also shrinks a column's minimum, so on a
   phone "no subscription" broke into "subscri / ption" instead of the table
   scrolling sideways in its wrapper. */
.dense .tbl td{overflow-wrap:break-word}
.dense .tbl td.prose{min-inline-size:16ch}
.dense .tbl td.ts,.dense .tbl td.num{overflow-wrap:normal}
/* Numeric cells and their headers, not every .num: the server id under a
   server's name is a .num too and hung off the name's end edge. */
.dense .tbl :is(th,td).num{text-align:end}
/* C-admin-24: the caption gets the width, the sparkline a fixed track, so a
   row of tiles keeps one caption edge. */
.dense .kpi-f{align-items:flex-start}
.dense .kpi-f .kpi-d{flex:1 1 auto}
.dense .kpi-f .spark{flex:0 0 var(--space-16)}
/* C-admin-25: two tables in one panel are two blocks, not one broken grid. */
.dense .tbl-wrap + .tbl-wrap{margin-top:var(--space-gap-xl)}
/* C-admin-43: one block gap, never below the card padding. */
.main.bleed.dense .wg{gap:var(--space-gap-xl);margin-bottom:var(--space-gap-xl)}
.dense .panel + .panel,.dense .stats + .panel{margin-top:var(--space-gap-xl)}
.dense .wg > .panel + .panel{margin-top:0}
/* C-admin-26: the drawer's primary action stays in view at its foot. */
#drawer .foot{position:sticky;inset-block-end:calc(var(--space-6) * -1);margin:var(--space-gap-xl) calc(var(--space-6) * -1) calc(var(--space-6) * -1);
  padding:var(--space-3) var(--space-6);background:var(--color-surface-card);border-top:1px solid var(--color-border-default);z-index:1}
#drawer .foot .callout{margin-bottom:var(--space-3)}
/* The row action stays in view while the table scrolls under it. */
.dense .tbl .act{position:sticky;inset-inline-end:0;background:var(--color-card-bg);border-inline-start:1px solid var(--color-table-border)}
.dense .tbl tbody tr:hover .act{background:var(--color-card-bg-hover)}
/* A phone keeps a plot, not a strip. */
.dense .chartbox svg{min-block-size:11.25rem}
/* 640 to 1099: Cost leaders gets its own row; four tiles go 2 by 2. */
@media (min-width:640px) and (max-width:1099px){.dense .w4{grid-column:span 12}}
.dense .stats{grid-template-columns:repeat(2,minmax(0,1fr))}
@media (min-width:72rem){.dense .stats{grid-template-columns:repeat(4,minmax(0,1fr))}}
@media (max-width:35rem){.dense .stats{grid-template-columns:1fr}}

/* Section label, Supabase's move: a small mono uppercase caption reads as
   instrument labelling rather than another heading competing with the data. */
.cap{font-family:var(--font-mono);font-size:var(--text-xs);text-transform:uppercase;
  letter-spacing:var(--tracking-wide);color:var(--color-text-tertiary);font-weight:var(--weight-semibold)}
[dir=rtl] .cap{letter-spacing:normal;text-transform:none;font-family:var(--font-arabic)}

/* Heatmap: 7 rows x 24 hours. The single best widget for a chat product,
   and it only reads if the hours are local. */
/* 24 columns is a lot for a phone. minmax(0,1fr) lets the cells shrink rather
   than overflow, and below the breakpoint where they would become unreadable
   the grid scrolls sideways inside its own panel instead of pushing the page. */
.heat-wrap{overflow-x:auto;overscroll-behavior-x:contain;max-inline-size:var(--size-content-max)}
.heat{display:grid;grid-template-columns:auto repeat(24,minmax(0,1fr));gap:var(--width-focus);align-items:center;direction:ltr}
.heat-key{display:flex;align-items:center;justify-content:flex-end;gap:var(--space-2);margin-top:var(--space-2);
  font-size:var(--text-xs);color:var(--color-text-tertiary)}
.heat-key i{display:block;width:var(--space-3);height:var(--space-3);border-radius:var(--radius-sm)}
@media (max-width:40rem){
  .heat{grid-template-columns:auto repeat(24,minmax(var(--space-3),1fr));min-width:max-content}
  /* Two-hour columns (set by the renderer): twelve fit a phone, no scrolling. */
  .heat.h12{grid-template-columns:auto repeat(12,minmax(0,1fr));min-width:0}
}
.heat .hl{font-size:var(--text-xs);color:var(--color-text-tertiary);padding-inline-end:var(--space-2);
  font-family:var(--font-mono);text-align:end}
/* No cursor:pointer and no hover ring: these cells carry a title and nothing
   else, and a pointer on something that does not respond is a broken promise. */
.heat .hx{font-size:var(--text-xs);color:var(--color-text-tertiary);white-space:nowrap;overflow:visible;font-variant-numeric:tabular-nums;direction:ltr}
.heat i{aspect-ratio:1;border-radius:var(--radius-sm);background:var(--color-surface-sunken);
  display:block}

/* Numbers never jiggle on refresh. */
.tbl,.heat{font-variant-numeric:tabular-nums}

/* The area chart uses preserveAspectRatio="none" so it fills its box, which
   also scales the stroke and any circle with it. The line keeps its width via
   vector-effect; the end dot cannot, so it lives outside the SVG's coordinate
   space as a positioned element and stays round at every width. */
.chart-plot .dot-last{position:absolute;width:var(--space-2);height:var(--space-2);border-radius:var(--radius-full);
  background:var(--color-surface-page);border:var(--width-focus) solid var(--color-text-brand);
  transform:translate(50%,-50%);pointer-events:none}


/* A timestamp that wraps to four lines triples the row height and buys
   nothing: every console table already scrolls sideways inside .tbl-wrap. */
.tbl .ts{white-space:nowrap}
/* An id such as T-5001 is one token: the hyphen is not a line break. */
.tbl td code{white-space:nowrap}
/* A member with no captured name shows their Discord id. Unbroken, nineteen
   digits pushed the Risk column out of view on a phone; there it is cut with an
   ellipsis, and the full id is still what a selection copies. */
@media (max-width:39.9375rem){.tbl td .cell code{display:inline-block;max-inline-size:8ch;overflow:hidden;
  text-overflow:ellipsis;vertical-align:bottom}}
/* A long filename stays on one line and gives its full name on hover. */
.tbl .src-name{display:inline-block;max-inline-size:18rem;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;vertical-align:bottom}
/* A command response keeps a readable measure inside the sideways scroller
   instead of shrinking to its longest word. */
#cmdTable td:nth-child(2){min-inline-size:14rem}
/* Numeric columns: digits are left to right in both languages, so the units
   digit lines up on the physical right, and the header sits over it. */
.tbl :is(th,td).n{direction:ltr;text-align:right}
.tbl .lb-rank{font-family:var(--font-mono)}
/* /dash is read on wide monitors too: cap the column so a label is not a
   screen away from its value. The cap is the console width, and the column
   keeps to the start edge like the operator console: at 2560 wide the old
   centred cap left two bands of empty page, one of them against the sidebar. */
.main:not(.bleed) > *{max-inline-size:var(--size-console-max);margin-inline:0 auto}
.field .hint{max-inline-size:var(--size-prose-max)}
/* Positive change is its own class so it never inherits the flat grey. */
.stat .delta.up{color:var(--color-feedback-success)}
/* Knowledge: paste box labels and actions, the upload status line, and no
   empty-state art competing with an open paste box. */
.kb-lbl{font-size:var(--text-xs);color:var(--color-text-secondary);font-weight:var(--weight-medium)}
.kb-paste input.input{width:100%}
.kb-paste-acts{display:flex;gap:var(--space-gap-sm);flex-wrap:wrap}
.kb-status{padding:var(--space-3) var(--space-4);font-size:var(--text-xs);color:var(--color-text-secondary);
  border-bottom:1px solid var(--color-border-subtle)}
#kbPaste:not([hidden]) ~ #kbTable .empty{display:none}
.lb-off{margin:0;padding:var(--space-3) var(--space-4);font-size:var(--text-sm);color:var(--color-text-secondary);
  border-bottom:1px solid var(--color-border-subtle)}
/* The destructive confirmation reuses the server switcher's dialog. */
#cfmScrim{z-index:var(--z-modal-scrim)}
.cfm-dialog{padding-block-end:var(--space-5)}
.cfm-msg{padding-inline:var(--space-5);color:var(--color-text-secondary);font-size:var(--text-sm)}
.cfm-quote{margin:var(--space-3) var(--space-5) 0;padding:var(--space-3);border-inline-start:1px solid var(--color-border-default);
  color:var(--color-text-tertiary);font-size:var(--text-xs);overflow-wrap:anywhere}
.cfm-acts{display:flex;gap:var(--space-gap-sm);padding:var(--space-5) var(--space-5) 0;flex-wrap:wrap}
/* Once .panel-head wraps, an end-aligned note sits ragged under a start-aligned
   heading and reads as mis-centred. */
@media (max-width:40rem){.panel-head .note{text-align:start;width:100%}}


/* The Health view was a single short panel holding four chips on a 1920 screen.
   The checks lay out as cards that use the width they are given. */
.pills{display:grid;grid-template-columns:repeat(auto-fit,minmax(13rem,1fr));
  gap:var(--space-gap-md);align-items:stretch}
.pills .chip{justify-content:flex-start;min-height:var(--size-control-xl);
  padding:var(--space-4);border-radius:var(--radius-card);font-size:var(--text-sm)}

.kb-paste {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border-subtle);
}
.kb-paste textarea {
  width: 100%;
  resize: vertical;
}

/* ── /dash/login: the customer's split welcome ────────────────────────── */
/* Distinct from .login / .login-card above, which stay exactly as they were
   for /admin/login. The operator console is an internal tool and reads like
   one; this is the first screen a paying server owner sees.

   The art sits physically on the LEFT in both directions, which is the single
   place in this codebase that overrides logical order. The approved
   illustration paints its characters into the left third and leaves the right
   two thirds quiet for this panel, and the art direction forbids mirroring the
   characters to suit a layout. So the grid is forced to physical order and the
   panel's own text direction is restored from the document. */
.login-body{display:block;height:auto;overflow:auto}
.login2{direction:ltr;min-height:100dvh;display:grid;grid-template-columns:1.15fr 1fr}
[dir="rtl"] .login2-panel{direction:rtl}
[dir="ltr"] .login2-panel{direction:ltr}

.login2-art{position:relative;overflow:hidden;background:var(--color-surface-card)}
/* `cover` with the focus held to the start edge: the characters live there, and
   a centred cover crop drops the owl off a narrow column. */
.login2-art img{width:100%;height:100%;object-fit:cover;object-position:left 58%;display:block}
/* Dissolves the join between artwork and form instead of butting two surfaces
   together; the hairline that used to mark it is gone with it. */
.login2-art::after{content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(to right, transparent 58%, var(--color-surface-page) 100%),
             linear-gradient(to bottom, color-mix(in srgb, var(--color-surface-page) 55%, transparent) 0%,
               transparent 12%, transparent 88%,
               color-mix(in srgb, var(--color-surface-page) 55%, transparent) 100%)}

.login2-panel{display:grid;place-items:center;padding:var(--space-10) var(--space-8);
  background:var(--color-surface-page);position:relative}

/* The form is a card, the way a sign-in form is expected to be. It had been
   floating loose on the panel: correct alignment, no object. A defined edge
   also gives the eye somewhere to land when the half beside it is a full
   illustration, and it matches the operator card and the checkout panes so
   every form on the product reads as the same kind of thing. */
.login2-inner{width:min(100%, 23rem);background:var(--color-card-bg);
  border:1px solid var(--color-card-border);border-radius:var(--radius-panel);
  padding:var(--space-8) var(--space-7);box-shadow:var(--shadow-lg)}
/* gap:0, like landing/site.css: the wordmark is a bare text node and so its own
   anonymous flex item, which means a gap lands between "ngbot" and ".ai" too.
   The icon gets its spacing from a margin instead. */
.login2-inner .brand{display:inline-flex;align-items:center;gap:0;
  font-family:var(--font-display);font-weight:var(--weight-bold);font-size:var(--text-lg);
  color:var(--color-text-primary);direction:ltr;margin-bottom:var(--space-gap-xl);
  text-decoration:none}
.login2-inner .brand img{width:var(--size-icon-xl);height:var(--size-icon-xl);margin-inline-end:var(--space-gap-sm)}
.login2-inner h1{font-size:var(--text-3xl);line-height:var(--leading-tight);
  margin-bottom:var(--space-gap-sm)}
.login2-sub{color:var(--color-text-secondary);font-size:var(--text-base);
  line-height:var(--leading-relaxed);margin-bottom:var(--space-gap-xl);max-width:var(--size-prose-max)}
.login2-err,.login2-notice{margin-bottom:var(--space-gap-lg)}
.login2-go{margin-bottom:var(--space-gap-md)}
.login2-go.is-busy{opacity:var(--opacity-disabled);pointer-events:none}
.login2-note{color:var(--color-text-tertiary);font-size:var(--text-sm);
  line-height:var(--leading-relaxed);margin-bottom:var(--space-gap-xl)}

/* Operator entry: present, labelled, and plainly not the customer's path. */
.login2-op{border-top:1px solid var(--color-border-default);padding-top:var(--space-gap-lg);
  margin-top:var(--space-gap-lg)}
.login2-op summary{color:var(--color-text-tertiary);font-size:var(--text-sm);cursor:pointer;
  list-style:none;display:inline-flex;align-items:center;gap:var(--space-gap-xs);
  border-radius:var(--radius-sm);padding:var(--space-1) var(--space-2);
  margin-inline-start:calc(var(--space-2) * -1)}
.login2-op summary::-webkit-details-marker{display:none}
.login2-op summary::before{content:"";width:var(--size-icon-xs);height:var(--size-icon-xs);
  border-inline-end:1px solid currentColor;border-block-end:1px solid currentColor;
  transform:rotate(-45deg);transition:transform var(--duration-base) var(--ease-out);flex:none}
[dir="rtl"] .login2-op summary::before{transform:rotate(-135deg)}
.login2-op[open] summary::before{transform:rotate(45deg)}
.login2-op summary:hover{color:var(--color-text-secondary)}
.login2-op summary:focus-visible{outline:var(--width-focus) solid var(--color-border-focus);
  outline-offset:var(--width-focus)}
.login2-op form{margin-top:var(--space-gap-md)}
.login2-oplead{color:var(--color-text-tertiary);font-size:var(--text-xs);
  margin-bottom:var(--space-gap-md)}

.login2-links{display:flex;flex-wrap:wrap;gap:var(--space-gap-md);
  margin-top:var(--space-gap-xl);font-size:var(--text-sm)}
/* Same floor as the marketing footer: these measured 23px, well under the
   control height every button on this surface meets. */
.login2-links a,.login2-op summary{display:inline-flex;align-items:center;
  min-height:var(--size-control-md)}
.login2-links a{color:var(--color-text-tertiary);text-decoration:none}
.login2-links a:hover{color:var(--color-text-secondary);text-decoration:underline}
.login2-lang{margin-top:var(--space-gap-lg)}

/* Phone: a compact band of art above the panel, never a full screen of it.
   The sign-in button has to stay within reach of the first viewport. */
@media (max-width:52rem){
  .login2{grid-template-columns:1fr;min-height:100dvh}
  .login2-art{height:min(30dvh, 13rem)}
  .login2-art img{object-position:center 38%}
  .login2-panel{padding:var(--space-5) var(--space-4) var(--space-8);place-items:start center}
  /* On a phone the card keeps its edge but gives back the padding it does not
     need at that width. */
  .login2-inner{width:100%;padding:var(--space-6) var(--space-5)}
  .login2-inner h1{font-size:var(--text-2xl)}
  .login2-inner .brand{margin-bottom:var(--space-gap-lg)}
  .login2-sub{margin-bottom:var(--space-gap-lg)}
}

/* ── State illustrations ──────────────────────────────────────────────── */
/* ui.css sizes .empty img to an avatar box, which is right for the mascot
   poses it was written for and wrong for a 3:2 scene: it would squash them
   into a square. These are the approved flat illustrations, painted at the
   widths the placement map gives, contained so no face or ear is ever cropped,
   with the aspect ratio reserved up front so nothing jumps when they land. */
/* Rounded, because these scenes are opaque charcoal rectangles rather than
   cutouts. Against the card surface the straight edge reads as a rendering
   accident; a radius makes it a deliberate bounded illustration panel, which
   is what the art direction asks for when the background cannot be matched
   exactly. */
.empty-art{max-width:var(--size-prose-max);margin-inline:auto}
/* The well is gone. An inset box that matched the artwork still read as a
   separate square, which is what the review kept objecting to, and matching a
   colour was never the same thing as removing a rectangle.

   Instead the scene sits directly on the panel and the image alone is blended
   with `lighten`: every pixel darker than the panel becomes the panel, so the
   artwork's own near-black ground disappears and the characters, which are all
   lighter than it, stay. `isolation:isolate` keeps the blend inside this
   component so it can never reach a heading, a button or anything else.

   The backdrop has to be painted HERE, because that is what the blend composites
   against; inheriting it is not enough. Most hosts are a panel or the switcher
   dialog; the ones that sit on the page surface (the shell, the KPI grid, the
   modules grid, billing and settings) frame it as a panel below, or it would be
   a square card-coloured rectangle.

   What this does not do: it is not alpha extraction. Contact shadows under the
   characters and dark interior fills (the book box in empty.png, the lens in
   search-empty.png) are darker than the panel too, so they lift with the
   background and those areas flatten. Characters, outlines and eyes survive.
   Verified per asset; the durable fix is transparent cutouts, which this
   package did not supply and which must not be improvised by regenerating art. */
.empty-art{max-width:var(--size-prose-max);margin-inline:auto;
  isolation:isolate;background:var(--color-card-bg)}
:is(#shellState,#statTiles,#modules,#billingBody,#settingsState) > .empty-art{border-radius:var(--radius-panel);
  box-shadow:0 0 0 var(--width-hairline) var(--color-card-border);padding:var(--space-card)}
.stats > .empty-art,.modules > .empty-art{grid-column:1 / -1}
/* The art frame, its sizes and the blend itself live in "v2 · States". */
.empty-quiet{padding-block:var(--space-8)}

/* ── Server switcher ──────────────────────────────────────────────────── */
.srv-dialog{position:fixed;z-index:var(--z-modal);inset-block-start:50%;inset-inline-start:50%;
  transform:translate(-50%, -50%);width:min(92vw, 30rem);max-height:min(80dvh, 34rem);
  display:flex;flex-direction:column;background:var(--color-surface-card);
  border:1px solid var(--color-border-default);border-radius:var(--radius-modal);
  box-shadow:var(--shadow-overlay);overflow:hidden}
/* translate(-50%) is physical, so in RTL the inline-start offset flips but the
   transform does not; this puts it back on centre. */
[dir="rtl"] .srv-dialog{transform:translate(50%, -50%)}
.srv-head{display:flex;align-items:center;justify-content:space-between;gap:var(--space-gap-md);
  padding:var(--space-5) var(--space-5) var(--space-gap-md)}
.srv-head h2{font-size:var(--text-lg);font-family:var(--font-display)}
.srv-search{position:relative;padding:0 var(--space-5) var(--space-gap-md)}
.srv-search .icon{position:absolute;inset-block-start:50%;transform:translateY(-50%);
  inset-inline-start:var(--space-8);width:var(--size-icon-sm);height:var(--size-icon-sm);
  color:var(--color-text-tertiary);pointer-events:none;margin-top:calc(var(--space-gap-md) / -2)}
.srv-search input{width:100%;min-height:var(--size-control-md);
  padding-inline:var(--space-10) var(--space-4);background:var(--color-surface-page);
  border:1px solid var(--color-border-control);border-radius:var(--radius-input);
  color:var(--color-text-primary);font-size:var(--text-sm)}
.srv-search input:focus-visible{outline:var(--width-focus) solid var(--color-border-focus);
  outline-offset:var(--width-focus);border-color:var(--color-border-focus)}
.srv-list{overflow-y:auto;padding:0 var(--space-3) var(--space-3);display:flex;
  flex-direction:column;gap:var(--space-1)}
.srv-opt{display:flex;align-items:center;gap:var(--space-gap-md);padding:var(--space-3);
  border-radius:var(--radius-button);cursor:pointer;border:1px solid transparent;
  transition:background var(--duration-fast)}
.srv-opt:hover{background:var(--color-interactive-hover-overlay)}
.srv-opt:focus-visible{outline:var(--width-focus) solid var(--color-border-focus);outline-offset:calc(var(--width-focus) * -1)}
/* The current server is marked by a border, a label and aria-selected, never by
   colour alone. */
.srv-opt.on{background:var(--color-interactive-selected-bg);
  border-color:var(--color-interactive-selected-border)}
.srv-meta{flex:1;min-width:0}
.srv-meta b{display:block;color:var(--color-text-primary);font-size:var(--text-sm);
  font-weight:var(--weight-semibold);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
.srv-meta span{font-size:var(--text-xs);color:var(--color-text-tertiary)}
.srv-now{font-size:var(--text-xs);color:var(--color-text-brand);font-weight:var(--weight-medium);flex:none}
.srv-none{list-style:none}

@media (max-width:40rem){
  .srv-dialog{width:100vw;max-width:100vw;max-height:88dvh;inset-block-end:0;inset-block-start:auto;
    inset-inline-start:0;transform:none;border-radius:var(--radius-modal) var(--radius-modal) 0 0}
  [dir="rtl"] .srv-dialog{transform:none}
}

/* ── First-run checklist ──────────────────────────────────────────────── */
.setup{margin-bottom:var(--space-gap-lg)}
.setup-steps{display:flex;flex-direction:column;gap:var(--space-1)}
.setup-steps li{display:flex;align-items:center;gap:var(--space-gap-md);padding:var(--space-3);
  border-radius:var(--radius-button);border:1px solid transparent}
.setup-steps li:not(.done){background:var(--color-interactive-hover-overlay);
  border-color:var(--color-border-default)}
.setup-num{width:var(--size-avatar-sm);height:var(--size-avatar-sm);border-radius:var(--radius-full);
  display:grid;place-items:center;flex:none;font-size:var(--text-xs);font-weight:var(--weight-bold);
  background:var(--color-surface-raised);color:var(--color-text-tertiary);
  border:1px solid var(--color-border-default)}
/* Done is icon plus the word, never the tick alone and never colour alone. */
.done .setup-num{background:var(--color-feedback-success-bg);color:var(--color-feedback-success-icon);
  border-color:var(--color-feedback-success-border)}
.setup-num .icon{width:var(--size-icon-xs);height:var(--size-icon-xs)}
.setup-txt{flex:1;min-width:0}
.setup-txt b{display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
  color:var(--color-text-primary)}
.setup-txt span{font-size:var(--text-xs);color:var(--color-text-tertiary)}
.setup-state{font-size:var(--text-xs);color:var(--color-text-tertiary);flex:none}
.done .setup-state{color:var(--color-feedback-success-text)}
@media (max-width:40rem){
  .setup-steps li{flex-wrap:wrap}
  .setup-steps .btn{width:100%}
}
/* The status reads down one edge: after the optional button, not before it. */
@media (min-width:40.01rem){.setup-state{order:1}}

/* A measurement that does not exist reads as absent, not as a small number. */
.kpi .kpi-na{font-size:var(--text-base);color:var(--color-text-tertiary);
  font-family:var(--font-sans);font-weight:var(--weight-medium)}
.kpi.muted{opacity:var(--opacity-muted)}

/* Shown only on the refusal it answers, so it sits above the sign-in button
   rather than competing with it on the normal path. */
.login2-invite{margin-bottom:var(--space-gap-sm)}

/* ── Topbar hierarchy ─────────────────────────────────────────────────── */
/* Server, page title and the page's own action lead; heartbeat, freshness,
   refresh, search and language are utilities and now read as a set rather
   than as six peers competing with the title. "Open in Discord" is an icon
   button: it is useful, but it should not be the widest control on the
   working view of an already-connected server. */
.topbar-actions{display:flex;align-items:center;gap:var(--space-gap-sm);flex-wrap:wrap;
  justify-content:flex-end}
.topbar-actions .beat,.topbar-actions .chip{order:-1}
.crumbs h1{font-size:var(--text-xl);line-height:var(--leading-tight)}

@media (max-width:52rem){
  /* Phone: identity on its own row, utilities beneath it, nothing squeezed. */
  .topbar{flex-direction:column;align-items:stretch;gap:var(--space-gap-md)}
  .topbar-actions{justify-content:flex-start;gap:var(--space-gap-xs)}
  .topbar-actions .search{order:1;flex:1 1 100%}
  .topbar-actions .search input{width:100%}
}

/* ── Overview summary on small screens ────────────────────────────────── */
/* Four full-height cards stacked one per row pushed the first useful content
   below two screens of KPIs. Below 52rem they become compact rows: label and
   value on one line, the supporting delta under it. */
@media (max-width:52rem){
  .stats{grid-template-columns:1fr 1fr;gap:var(--space-2);margin-bottom:var(--space-gap-lg)}
  .stats .stat{padding:var(--space-3)}
  .stats .stat .val,.stats .stat .usage-pct{font-size:var(--text-xl)}
  .stats .stat .head{font-size:var(--text-xs)}
  .stats .stat .head .ic{display:none}
  .stats .stat .delta{font-size:var(--text-xs)}
}
@media (max-width:26rem){
  /* Long Arabic labels stop fitting two-up at this width, so one compact row
     per metric: label and value on a line, the comparison under it. */
  .stats{grid-template-columns:1fr}
  .stats .stat{display:flex;flex-direction:row;flex-wrap:wrap;align-items:baseline;
    justify-content:space-between;gap:var(--space-gap-xs) var(--space-gap-sm)}
  .stats .stat .usage-pct{font-size:var(--text-lg)}
  .stats .stat .head{flex:1 1 auto;margin-bottom:0}
  .stats .stat .val{font-size:var(--text-lg)}
  .stats .stat .delta{flex:1 1 100%}
}


/* The axis row runs with the line, not with the page. */
.axis.ltr{direction:ltr;unicode-bidi:isolate}
.axis-unit{color:var(--color-text-tertiary);font-size:var(--text-xs)}
.axis-y{display:flex;justify-content:flex-end;font-size:var(--text-xs);
  color:var(--color-text-tertiary);font-variant-numeric:tabular-nums}

/* ── Knowledge panel header ───────────────────────────────────────────── */
.panel-lead{color:var(--color-text-secondary);font-size:var(--text-sm);
  line-height:var(--leading-relaxed);max-width:var(--size-prose-max)}
.panel-tools{display:flex;align-items:center;gap:var(--space-gap-sm);flex-wrap:wrap}

/* One Add source control; the two supported methods live inside it rather
   than competing as two buttons and a note. */
.addmenu{position:relative}
.menu{position:absolute;inset-block-start:calc(100% + var(--space-1));inset-inline-end:0;
  z-index:var(--z-drawer);min-inline-size:17rem;padding:var(--space-1);
  background:var(--color-surface-raised);border:1px solid var(--color-border-default);
  border-radius:var(--radius-lg);box-shadow:var(--shadow-overlay)}
/* Anchored to the button's end edge it hung off the start side whenever the
   tools row wrapped. On narrow screens it spans the panel head instead; on a
   wrapped desktop head setOpen() flips it to the start edge. */
.menu.flip{inset-inline-end:auto;inset-inline-start:0}
@media (max-width:52rem){
  .panel-head:has(.addmenu){position:relative}
  .addmenu{position:static}
  .menu{inset-inline:0;min-inline-size:0}
}
.menu-item{display:flex;align-items:flex-start;gap:var(--space-gap-sm);width:100%;
  padding:var(--space-3);border-radius:var(--radius-button);text-align:start;cursor:pointer;
  background:transparent;border:0;color:var(--color-text-primary)}
.menu-item:hover{background:var(--color-interactive-hover-overlay)}
.menu-item:focus-visible{outline:var(--width-focus) solid var(--color-border-focus);outline-offset:calc(var(--width-focus) * -1)}
.menu-item .icon{width:var(--size-icon-sm);height:var(--size-icon-sm);color:var(--color-text-tertiary);
  flex:none;margin-top:var(--space-1)}
.menu-item b{display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold)}
.menu-item small{display:block;font-size:var(--text-xs);color:var(--color-text-tertiary);
  line-height:var(--leading-relaxed)}

/* A row action, not a red block repeated down the table. The danger treatment
   belongs on the confirmation, where the decision is made. */
.row-del{color:var(--color-text-tertiary)}
.row-del:hover{color:var(--color-feedback-error);border-color:var(--color-feedback-error-border)}
.row-del .icon{width:var(--size-icon-sm);height:var(--size-icon-sm)}
.dim{color:var(--color-text-tertiary)}

/* ── Console header on small screens ──────────────────────────────────── */
/* Two rows, not one crowded strip: drawer and page identity first, utilities
   under it. The knowledge search is scoped to one page, so it does not claim
   the header on every other one, and the shortcut badge is hidden where there
   is no keyboard to press. */
@media (max-width:52rem){
  .topbar-lead{gap:var(--space-gap-sm)}
  .crumbs h1{font-size:var(--text-lg)}
  .topbar-actions .search{order:2;flex:1 1 100%}
  .kbd-hint{display:none}
}
@media (hover:none){
  .kbd-hint{display:none}
}
/* Below the drawer breakpoint the knowledge search only belongs on its own
   page; elsewhere it is noise in a header that has a title to show. */
@media (max-width:52rem){
  body:not(.view-knowledge) .topbar-actions .search{display:none}
}

/* ── Usage as a proportion ────────────────────────────────────────────── */
/* A ratio drawn as a ratio: the percentage leads, the raw numbers support it,
   and the bar carries the shape. The bar clamps at full while the numbers keep
   telling the truth, so going past the allowance is visible rather than hidden
   behind a track that cannot grow. */
/* min-inline-size:0 on the tile and its caption: without it the caption's flex
   basis is its longest unbroken run, which at 360 and 390 pushed the line past
   the tile's own edge. */
.stat.usage{display:flex;flex-direction:column;align-items:stretch;gap:var(--space-gap-sm);min-inline-size:0}
/* Arabic sentences in the mono slots (tile sub-lines, axis captions, relative
   times, level chips) take the Arabic face, spaces included. */
[dir=rtl] .stat .delta,[dir=rtl] .kpi-d,[dir=rtl] .chart .axis,[dir=rtl] .act .ts,
[dir=rtl] .lb-lvl,[dir=rtl] .setup-state{font-family:var(--font-arabic)}
.stat.usage .delta{min-inline-size:0;overflow-wrap:anywhere;flex:0 1 auto}
.stat.usage .usage-ratio{white-space:nowrap}
.usage-top{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:var(--space-gap-sm)}
.usage-pct{font-family:var(--font-display);font-weight:var(--weight-bold);
  font-size:var(--text-3xl);color:var(--color-text-primary);line-height:var(--leading-none);
  font-variant-numeric:tabular-nums}
/* Words in the tile's face ("549 of 1,000" / "549 من 1,000"); only the numbers
   are tabular. A mono ratio widened the spaces of the Arabic word. */
.usage-ratio{font-size:var(--text-sm);color:var(--color-text-tertiary)}
.usage-ratio .num,#kbQuota .num{font-variant-numeric:tabular-nums;color:var(--color-text-secondary)}
.usage-bar{height:var(--space-2);background:var(--color-surface-page);
  border-radius:var(--radius-full);overflow:hidden}
.usage-bar i{display:block;height:100%;background:var(--color-action-primary);
  border-radius:var(--radius-full);
  transition:inline-size var(--duration-moderate) var(--ease-out)}
/* Status is never colour alone: the caption line already names the state. */
.stat.usage.near .usage-bar i{background:var(--color-feedback-warning)}
.stat.usage.over .usage-bar i{background:var(--color-feedback-error)}
.stat.usage.over .usage-pct{color:var(--color-feedback-error-text)}
@media (prefers-reduced-motion:reduce){.usage-bar i{transition:none}}

/* Billing: label and value within one eye span, not the panel's full width. */
#billingBody .kv{justify-content:flex-start;gap:var(--space-gap-lg)}
#billingBody .kv > span{flex:0 1 12rem}
#billingBody .kv > b{text-align:start}

/* Phone and drawer widths: the menu button is the only way to the other
   views, and it scrolled away with the page (1,850 to 2,400 down from the
   module switches). The identity row sticks to the top of .main; the
   utilities under it still scroll, so the stuck bar stays one row tall. */
@media (max-width:900px){
  .topbar{display:contents}
  .topbar-lead{position:sticky;inset-block-start:calc(var(--space-4) * -1);z-index:var(--z-sticky);
    margin:calc(var(--space-4) * -1) calc(var(--space-4) * -1) var(--space-gap-md);
    padding:var(--space-2) var(--space-4);background:var(--color-surface-page);
    border-bottom:1px solid var(--color-border-default)}
  .topbar-actions{margin-bottom:var(--space-gap-xl)}
}

/* Beside the sidebar, below 75rem the main column is too narrow for a 2fr/1fr
   pair: the chart fell to a third of its width and every Top-members name to
   one glyph. Stack the pair until both halves fit. */
@media (max-width:75rem){
  .row,.row.split{grid-template-columns:minmax(0,1fr)}
}

/* Arabic sentences are not code. JetBrains Mono has no Arabic glyphs, so these
   runs fell back letter by letter and read detached. The digits inside them
   keep the mono face through their own .num/.ltr wrappers. */
[dir=rtl] .crumbs,[dir=rtl] .stat .delta,[dir=rtl] .kpi-d,[dir=rtl] .beat,
[dir=rtl] .pager .info{font-family:var(--font-arabic)}
[dir=rtl] :is(.crumbs,.stat .delta,.kpi-d,.beat,.pager .info) :is(.num,.ltr,bdi){font-family:var(--font-mono)}

/* The short-viewport cap on state art (phone landscape, 200% and 400% zoom)
   moved to "v2 · States" with the frame it caps. */

/* ── Shell fit (d-shell 3) ─────────────────────────────────────────── */
/* Server names: the crumb ellipsizes instead of pushing the page wide, and
   every name (dir=auto in the markup) cuts at its own logical end. */
.topbar-lead,.crumbs{min-width:0}
#crumbServer{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}
.server-name b,.srv-meta b,#crumbServer{text-align:start}
/* The header search shrinks before the utilities wrap to a second row. */
@media (max-width:60rem){
  .topbar-actions .search{min-width:0;flex:1 1 0}
}
@media (max-width:22.5rem){
  #lastSync{display:none}
}
/* iOS zooms any focused field under the base size; coarse pointers get it. */
@media (hover:none) and (pointer:coarse){
  .field input,.field select,.field textarea,.input,.search input,.srv-search input{font-size:var(--text-base)}
}

/* The area fill fades from a token, not a literal in the script. */
.area-top{stop-opacity:var(--opacity-chart-area)}

/* Level rewards (Settings): a level and a role per row, the remove action at
   the row's end. The level box is sized to three digits, the role takes the
   rest of the row and wraps under it on a phone. */
.lr-list{display:flex;flex-direction:column;gap:var(--space-gap-md);margin-block:var(--space-gap-md)}
.lr-list:empty{display:none}
.lr-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--space-gap-md)}
.lr-field{display:flex;flex-direction:column;gap:var(--space-gap-xs);font-size:var(--text-sm);color:var(--color-text-secondary)}
.lr-level{inline-size:9ch}
.lr-grow{flex:1 1 auto;min-inline-size:0}
.lr-grow select{inline-size:100%}
#permWarn{margin-bottom:var(--space-gap-lg)}

/* Custom commands: row actions and the trigger field with its fixed "!" */
.row-acts{display:inline-flex;gap:var(--space-gap-sm);flex-wrap:wrap;justify-content:flex-end}
.cmd-trigger{display:flex;align-items:center;gap:var(--space-2);inline-size:100%;direction:ltr}
.cmd-trigger .input{flex:1;min-inline-size:0;font-family:var(--font-mono)}
.cmd-trigger .input[readonly]{color:var(--color-text-secondary)}
.cmd-bang{font-family:var(--font-mono);color:var(--color-text-tertiary);font-weight:var(--weight-semibold)}
#cmdForm:not([hidden]) ~ #cmdTable .empty{display:none}
.tbl td.col-cat{white-space:nowrap}
#wcNoChan{margin-bottom:var(--space-gap-lg)}
#permWarn p{margin:0}
#permWarn p + p{margin-top:var(--space-2)}
#permWarn{flex-direction:column}
/* A purchase that just went through (upgradedNote): the words, then Dismiss
   at the row's end, one control high. */
#upgradedNote{margin-bottom:var(--space-gap-lg)}
#upgradedNote .up-copy{flex:1 1 auto;min-inline-size:0}
#upgradedNote .up-copy p{margin:0;line-height:var(--leading-relaxed)}
#upgradedNote .up-copy p + p{margin-top:var(--space-1);color:var(--color-text-secondary)}
#upgradedNote .up-close{flex:none;margin-block:calc((var(--text-sm) * var(--leading-relaxed) - var(--size-control-md)) / 2) 0}
#upgradedNote .up-close .icon{color:inherit;margin:0}

/* A labelled switch inside a settings panel (the welcome card). */
.sw-row{display:flex;align-items:center;justify-content:space-between;gap:var(--space-gap-md)}
.sw-label{font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-primary)}

/* ═══════════════════════════════════════════════════════════════════════
   v2 · Shell and layout
   One content column, shared edges, one control height per row.
   ═══════════════════════════════════════════════════════════════════════ */
/* Operator console: the way into a customer's dashboard sits at a row's end
   and under the server's name in its drawer. */
.tbl td.row-end{text-align:end;white-space:nowrap}
.drawer-acts{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-gap-sm) var(--space-gap-md);
  margin-top:var(--space-gap-md)}
.drawer-acts .note{margin:0;font-size:var(--text-xs);color:var(--color-text-tertiary)}

/* ── Page header ──────────────────────────────────────────────────────── */
/* The crumb is a label, so it is set in the sans (it was the mono stack).
   The server name in it is painted through shownName() and carries a full
   sans stack of its own, truncating on the crumb's line; the page title
   always takes the next line. */
.crumbs{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--space-1);min-width:0}
.crumbs > h1{flex:1 0 100%;min-width:0}
/* A zero basis keeps a long name on the label's line (with its own basis it
   wrapped to a line of its own before it shrank) and lets it take the rest
   of that line. */
.crumbs > span:first-child{flex:none}
#crumbServer{flex:1 1 0%;min-width:0;font-family:var(--font-sans);font-weight:var(--weight-medium);
  color:var(--color-text-secondary)}
[dir=rtl] #crumbServer{font-family:var(--font-arabic)}
.crumbs h1{font-size:var(--text-2xl);line-height:var(--leading-tight)}
@media (max-width:52rem){.crumbs h1{font-size:var(--text-xl)}}
/* Desktop: the title and the toolbar are two columns. The title takes what
   the toolbar leaves and truncates, the toolbar keeps its width and sits on
   the title's line. A wrapping row dropped the toolbar under the title at
   some widths and not at others. */
@media (min-width:75.0625rem){
  .topbar{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;column-gap:var(--space-gap-xl)}
  .topbar-actions{flex-wrap:nowrap}
}
/* Beside the sidebar but short of that width the toolbar is its own row
   under the title: the status at the start, the controls at the end. */
@media (min-width:56.3125rem) and (max-width:75rem){
  .topbar{flex-direction:column;align-items:stretch;gap:var(--space-gap-md)}
}
/* The toolbar: a status group (live dot and freshness), then the controls.
   The status pushes the controls to the end edge whenever the row has room. */
.topbar-actions{display:flex;align-items:center;flex-wrap:wrap;gap:var(--space-gap-sm);justify-content:flex-end}
.topbar-status{display:flex;align-items:center;gap:var(--space-gap-md);min-height:var(--size-control-sm);
  margin-inline-end:auto;padding-inline-end:var(--space-gap-lg);white-space:nowrap}
.topbar-tools{display:flex;align-items:center;gap:var(--space-gap-sm)}
/* The freshness stamp is a caption after the live dot, set off by a hairline,
   not a chip competing with the buttons beside it. */
.topbar-status .chip{min-height:0;padding:0;padding-inline-start:var(--space-gap-md);border:0;
  border-inline-start:1px solid var(--color-border-default);border-radius:0;background:none;
  font-weight:var(--weight-regular);color:var(--color-text-tertiary);font-variant-numeric:tabular-nums}
/* The live dot sits in a slot as wide as its ring, so the ring stays inside
   the status box. At the page edge it spilled into the gutter on a phone. */
.beat::before{margin-inline:var(--space-1)}
.beat.pulse::after{inset-inline-start:var(--space-1);inset-block-start:50%;
  margin-block-start:calc(var(--space-2) / -2);animation-name:beat-ring-in}
@keyframes beat-ring-in{0%{opacity:.7;transform:scale(1)}70%{opacity:.12;transform:scale(2)}100%{opacity:0;transform:scale(2)}}
/* One control height across the toolbar: the small control on a desktop, the
   touch target on a phone or under any coarse pointer. Icon buttons are
   square. The search box is the same height as the buttons beside it. */
.topbar-actions .btn,.topbar-actions .search{block-size:var(--size-control-sm);min-height:var(--size-control-sm)}
.topbar-actions .btn-icon{inline-size:var(--size-control-sm);padding:0}
@media (max-width:52rem),(pointer:coarse){
  .topbar-actions .btn,.topbar-actions .search{block-size:var(--size-control-md);min-height:var(--size-control-md)}
  .topbar-actions .btn-icon{inline-size:var(--size-control-md)}
  .topbar-status{min-height:var(--size-control-md)}
}
/* On a phone the knowledge search is a row of its own under the status and
   the buttons. A later zero-basis rule for tablets had squeezed it into the
   button row, a box too narrow to read its own placeholder. */
@media (max-width:52rem){
  .topbar-status{padding-inline-end:0}
  .topbar-actions .search{order:2;flex:1 1 100%}
}
/* Below this width the language button is its icon alone, so it is square. */
@media (max-width:40rem){.topbar-actions .lang-btn{inline-size:var(--size-control-md);padding:0}}
/* The arrow leaves the box toward the end of the reading direction. */
[dir=rtl] #discordLink .icon,[dir=rtl] .icon.ext{transform:scaleX(-1)}

/* ── Sidebar ──────────────────────────────────────────────────────────── */
/* The wordmark keeps to the start edge in both directions (its own text
   stays left to right). It spanned the whole row, so on a phone the close
   button at the row's end sat on top of the logo in Arabic. Its inset
   matches the icon column of the rows under it. */
.side .brand{align-self:flex-start;padding-inline:calc(var(--space-3) + var(--width-hairline))}
.side-close{inset-block-start:calc(var(--space-5) + var(--space-1) + (var(--size-icon-xl) - var(--size-control-md)) / 2)}
/* Section labels are labels: sentence case and medium weight, on the same
   start edge as the rows under them, with more room above than below so each
   label reads with its own group. */
.side-section{text-transform:none;letter-spacing:normal;font-weight:var(--weight-medium);
  padding:var(--space-4) calc(var(--space-3) + var(--width-hairline)) var(--space-1)}
.side-nav > .side-section:first-child{padding-block-start:var(--space-1)}
/* A count is a number, not code: the label's face with tabular figures. */
.nav-item .count{font-family:var(--font-sans);font-variant-numeric:tabular-nums}
[dir=rtl] .nav-item .count{font-family:var(--font-arabic)}
/* A name marked dir=auto shapes in its own direction but lines up with the
   list it sits in: start resolved against the parent, so an Arabic server
   name in the English operator console starts at the same edge as the Latin
   ones, and a Latin name in the Arabic sidebar sits beside its icon. */
:is(.server-name b,.srv-meta b,#crumbServer,.lb-name,.drawer h3,.tbl td)[dir=auto]{
  text-align:-webkit-match-parent;text-align:match-parent}
/* Server picker: each option's icon starts on the dialog's text edge (the
   title and the search box), with the row's highlight reaching past it. */
.srv-list{padding-inline:calc(var(--space-5) - var(--space-3) - var(--width-hairline))}

/* ── Stat tiles and panels ────────────────────────────────────────────── */
/* One gap between blocks and one padding inside every card on a page, so
   content edges line up from card to card. */
.stats{gap:var(--space-gap-lg)}
.stat,.mod{padding:var(--space-card)}
@media (max-width:52rem){.panel,.stats .stat,.mod{padding:var(--space-card-sm)}}
/* Three tiles are laid out as three (the billing row: stats-3), four as four,
   or two by two where four do not fit. Below the width where three stop
   fitting side by side they stack. A skeleton or a state spans the row
   instead of sitting in its first cell. */
.stats.stats-3{grid-template-columns:repeat(3,minmax(0,1fr))}
@media (max-width:37.4375rem){
  .stats.stats-3{grid-template-columns:minmax(0,1fr)}
  /* Stacked, a tile without a bar is one line: its label, then its value at
     the end edge, the caption under both (the compact row the four-tile
     overview uses on the narrowest phones). */
  .stats.stats-3 .stat:not(.usage){flex-direction:row;flex-wrap:wrap;align-items:baseline;
    justify-content:space-between;gap:var(--space-gap-xs) var(--space-gap-sm)}
  .stats.stats-3 .stat:not(.usage) .head{flex:1 1 auto;min-height:0;margin-bottom:0}
  .stats.stats-3 .stat:not(.usage) .delta{flex:1 1 100%}
}
.stats > :is(.load,.empty,.callout),.modules > .load{grid-column:1 / -1}
/* Tile labels and captions are labels: sentence case, the sans. */
.stat .head,.kpi-l{text-transform:none;letter-spacing:normal;font-weight:var(--weight-medium)}
.stat .delta,.kpi-d,.act .ts,.lb-lvl,.chart .axis,.heat .hl{font-family:var(--font-sans);font-variant-numeric:tabular-nums}
[dir=rtl] :is(.crumbs,.stat .delta,.kpi-d,.beat,.pager .info) :is(.num,.ltr,bdi){font-family:var(--font-arabic)}
.cap{font-family:var(--font-sans);text-transform:none;letter-spacing:normal;font-weight:var(--weight-medium)}
/* Two rows of panels stack with the page gap between them, like any other
   pair of blocks (the answer row follows the activity row). */
.row + .row{margin-top:var(--space-gap-lg)}
/* Answer performance: three measures of one thing in one panel. They are
   columns of that panel, each led by its status rule, not three cards
   inside a card. */
.panel > .wg > .kpi{padding:0;padding-inline-start:var(--space-4);background:none;box-shadow:none;border-radius:0}
.panel > .wg > .kpi::before{opacity:1;inset-block:0;background:var(--accent,var(--color-border-default))}
/* The explanation reads straight under the number at the column's full
   width, and the verdict word closes the column, level across the row. Side
   by side in a third of a panel the explanation was one word a line. */
.panel > .wg > .kpi .kpi-f{display:contents}
.panel > .wg > .kpi .kpi-d{flex:none}
.panel > .wg > .kpi .kpi-d:is(.good,.warn,.bad){margin-top:auto}
/* The heatmap fills its panel like every other chart. */
.heat-wrap{max-inline-size:none}
/* Cells are square while the columns are narrow; once the main column is
   wide they keep one height and only widen: at the console width a square
   cell made the week half a screen tall. */
@media (min-width:75.0625rem){.heat i{aspect-ratio:auto;block-size:var(--space-7)}}
/* The one-line tile rows of the narrowest phones need no room reserved for
   a second label line. */
@media (max-width:26rem){.stats .stat:not(.usage) .head{min-height:0}}

/* ── Panel header, filters, tables, pager ─────────────────────────────── */
/* Title and tools share a baseline and the tools keep to the end edge; when
   the row is too narrow they wrap under the title, at its start edge. */
.panel-head{align-items:baseline}
.panel-head .search{align-self:center;min-height:var(--size-control-sm)}
/* A text link in a header (See all) is a control of the same height as the
   filter beside the next panel's title, so the two headers of a row agree. */
.panel-head .link{display:inline-flex;align-items:center;min-height:var(--size-control-sm)}
/* Filters, pager buttons and controls in a table row are one small-control
   height on a desktop and a touch target on a phone or a coarse pointer.
   Every small button is, the same way: a finger does not get smaller with
   the button. */
.seg button{min-height:var(--size-control-sm);padding:0 var(--space-3)}
.tbl .input{min-height:var(--size-control-sm)}
@media (max-width:52rem),(pointer:coarse){
  .seg button,.tbl .input,.btn-sm,.panel-head .search,.panel-head .link{min-height:var(--size-control-md)}
  .btn-icon.btn-sm{inline-size:var(--size-control-md)}
  .pager button{min-width:var(--size-control-md);height:var(--size-control-md)}
  .drawer .close{width:var(--size-control-md);height:var(--size-control-md)}
  .skip{display:inline-flex;align-items:center;min-height:var(--size-control-md)}
  /* A module switch keeps its drawn size, and its box grows to the touch
     target: the track moves to a pseudo element centred in that box. */
  .mod .sw,.mod .sw[aria-checked=false],.mod .sw:hover{block-size:var(--size-control-md);
    background:none;border-color:transparent}
  .mod .sw::before{content:"";position:absolute;inset-inline:0;inset-block-start:50%;
    block-size:var(--size-control-xs);margin-block-start:calc(var(--size-control-xs) / -2);
    border:1px solid transparent;border-radius:var(--radius-toggle);background:var(--color-switch-track-on)}
  .mod .sw[aria-checked=false]::before{background:var(--color-switch-track-off);border-color:var(--color-border-control)}
}
/* WebKit on iOS draws a native select at its own height and ignores
   min-height, so on the owner's iPhone every select was shorter than the
   text fields around it, and a level reward's level and role labels sat at
   two heights. A set height holds a select to the control height there. */
@media (max-width:52rem),(pointer:coarse){
  .field select,select.input{block-size:var(--size-control-md)}
}
/* A member with no captured name: on a phone the Discord id goes under the
   words instead of being cut to eight digits beside them. */
@media (max-width:39.9375rem){
  .tbl td .cell:has(> code){display:inline-grid;grid-template-columns:auto auto;align-items:center;
    column-gap:var(--space-gap-sm)}
  .tbl td .cell:has(> code) > .lb-av{grid-row:1 / 3}
  .tbl td .cell > code{max-inline-size:none;overflow:visible}
}
/* Clamped text says so with an ellipsis: the knowledge preview, and a long
   server name in the sidebar, which now gets two lines before it is cut. */
.kb-prev{text-overflow:ellipsis}
.server-name b{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;white-space:normal;
  overflow-wrap:anywhere;line-height:var(--leading-snug)}
/* Column headers are labels like the rest: sentence case. A column with no
   heading holds the row's actions and keeps to the row's end edge. */
.tbl th{text-transform:none;letter-spacing:normal;font-weight:var(--weight-medium)}
.tbl th.row-end{text-align:end}
/* A numeric column's heading is a label over numbers, not a number: the
   operator tables marked it .num and set "AI requests" in the mono face. */
.tbl th.num{font-family:var(--font-sans);font-size:var(--text-xs)}
[dir=rtl] .tbl th.num{font-family:var(--font-arabic)}
.pager .info{font-family:var(--font-sans);font-variant-numeric:tabular-nums}
.drawer section>h4{text-transform:none;letter-spacing:normal}

/* ── Operator console ─────────────────────────────────────────────────── */
/* Cost leaders: the name gets the first line; tier and share go under it,
   and the money with its share of the plan holds the end of both lines. In
   one flex line the names were cut to four letters on a laptop. */
.dense #topSpend .lb-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;
  gap:var(--space-1) var(--space-gap-md)}
.dense #topSpend .lb-name{grid-column:1 / 3;grid-row:1}
.dense #topSpend .lb-tier{grid-column:1;grid-row:2}
.dense #topSpend .share{grid-column:2;grid-row:2}
.dense #topSpend .lb-xp{grid-column:3;grid-row:1 / 3;text-align:end}
.dense #topSpend .lb-xp small{display:block}
/* Health checks fill their rows: a wrapping row whose chips grow, so four
   or five checks never leave a hole beside a lone last one (the grid put
   three and one, or three and two, depending on the width). */
.pills{display:flex;flex-wrap:wrap}
.pills > *{flex:1 1 13rem}
/* A header with a search box centres its title on the box; the title sat
   at the top of the taller box on its baseline. */
.panel-head:has(> .search){align-items:center}

/* ═══════════════════════════════════════════════════════════════════════
   v2 · Settings
   ═══════════════════════════════════════════════════════════════════════ */
/* The ngbot team's view of a customer's console (opened from /admin). */
.op-bar{display:flex;align-items:center;flex-wrap:wrap;gap:var(--space-gap-sm) var(--space-gap-md);
  max-inline-size:var(--size-console-max);margin-inline:auto;margin-bottom:var(--space-gap-lg);
  padding:var(--space-2) var(--space-4);border:1px solid var(--color-interactive-selected-border);
  background:var(--gradient-brand-soft);border-radius:var(--radius-lg);
  font-size:var(--text-sm);color:var(--color-text-secondary)}
.op-bar .icon{width:var(--size-icon-sm);height:var(--size-icon-sm);color:var(--color-text-brand);flex:none}
.op-bar span{flex:1 1 16rem;min-inline-size:0}
.op-bar .link{font-size:var(--text-sm)}
/* On touch the way back is a full-height target, like every other control;
   as a bare line of text it was a strip a fingertip could miss. */
@media (max-width:52rem),(pointer:coarse){
  .op-bar .link{display:inline-flex;align-items:center;min-height:var(--size-control-md)}
}

/* Settings: one column of sections aligned to the page title's start edge,
   the width a settings page reads at (a label a screen away from its control
   is what the two uneven cards used to produce on a wide monitor). */
#v-settings .set{max-inline-size:52rem;display:flex;flex-direction:column;gap:var(--space-gap-2xl);
  container-type:inline-size}
.set-sec{display:flex;flex-direction:column;gap:var(--space-gap-md)}
.set-head h2{font-size:var(--text-lg);font-weight:var(--weight-semibold);line-height:var(--leading-snug)}
.set-head p{margin-top:var(--space-1);font-size:var(--text-sm);color:var(--color-text-secondary);
  line-height:var(--leading-normal);max-inline-size:var(--size-prose-max)}
.set-head .set-lead{font-size:var(--text-xs);color:var(--color-text-tertiary)}
/* One surface per group; rows are separated by hairlines, not boxes. */
.set-card{background:var(--color-card-bg);border:1px solid var(--color-card-border);
  border-radius:var(--radius-panel);padding-inline:var(--space-card)}
.set-card.set-pad{padding-block:var(--space-card)}
.set-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,20rem);align-items:center;
  gap:var(--space-gap-md) var(--space-gap-2xl);padding-block:var(--space-gap-lg)}
.set-row + .set-row,.set-card > .callout + .set-row{border-top:1px solid var(--color-border-default)}
.set-row.set-stack{grid-template-columns:minmax(0,1fr)}
.set-row.set-switch{grid-template-columns:minmax(0,1fr) auto}
.set-meta{min-inline-size:0}
.set-meta label,.set-meta .set-label{display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
  color:var(--color-text-primary);line-height:var(--leading-snug)}
.set-meta .hint{margin-top:var(--space-1);font-size:var(--text-xs);color:var(--color-text-tertiary);
  line-height:var(--leading-normal);max-inline-size:var(--size-prose-max)}
/* A placeholder is one token: iOS Safari broke "{server}" across two lines
   inside its isolated LTR run. */
.set-meta .hint code{font-size:var(--text-xs);white-space:nowrap}
.set-ctl{min-inline-size:0}
.set-ctl.field{margin:0}
.set-switch .set-ctl{display:flex;justify-content:flex-end}
/* The welcome-card switch is a touch target on a phone, drawn at its usual
   size: the same treatment the module switches get in the shell section. */
@media (max-width:52rem),(pointer:coarse){
  .set-switch .sw,.set-switch .sw[aria-checked=false],.set-switch .sw:hover{block-size:var(--size-control-md);
    background:none;border-color:transparent}
  .set-switch .sw::before{content:"";position:absolute;inset-inline:0;inset-block-start:50%;
    block-size:var(--size-control-xs);margin-block-start:calc(var(--size-control-xs) / -2);
    border:1px solid transparent;border-radius:var(--radius-toggle);background:var(--color-switch-track-on)}
  .set-switch .sw[aria-checked=false]::before{background:var(--color-switch-track-off);border-color:var(--color-border-control)}
}
.set-card > .callout{margin-block:0 var(--space-gap-lg)}
.set-card > #h_health_lock{margin-block:var(--space-card) 0}
/* The Save bar stays at the foot of the view while the column scrolls, the
   unsaved-changes note (added beside the button by paintDirty) before it. */
.set-save{position:sticky;inset-block-end:0;z-index:var(--z-sticky);display:flex;align-items:center;
  justify-content:flex-end;gap:var(--space-gap-md);padding-block:var(--space-gap-md);
  margin-block-start:calc(var(--space-gap-lg) - var(--space-gap-2xl));
  background:var(--color-surface-page);border-top:1px solid var(--color-border-default);
  /* .main's bottom padding is part of the scrollport, so the column scrolled
     on under the bar; the shadow paints that strip with the page ground. */
  box-shadow:0 var(--space-6) 0 0 var(--color-surface-page)}
.set-save #settingsDirtyNote{order:-1}
/* A toast is fixed at the foot of the viewport, which is where this bar
   sticks: it covered the unsaved-changes note and, on a phone, Save itself,
   and an error toast stays until dismissed, so the next tap on Save only
   closed the toast. While the form is on screen the toast sits above the bar
   (its height: the hairline, two gaps and one control, plus a gap). */
body:has(#v-settings.active > .set:not([hidden])) .toast{
  bottom:calc(var(--space-6) + var(--size-control-md) + var(--space-gap-md) * 3)}
/* Level rewards: level, role and Remove on one line, all one control high. */
#lrPanel .lr-list{margin-block:0 var(--space-gap-lg)}
#lrPanel .lr-row{display:grid;grid-template-columns:8rem minmax(0,1fr) auto;align-items:end;gap:var(--space-gap-md)}
#lrPanel .lr-field .input{inline-size:100%}
#lrPanel .lr-del{min-height:var(--size-control-md)}
#lrPanel #lrUnavailable{margin-block:0 var(--space-gap-md)}
@container (max-width:40rem){
  .set-row{grid-template-columns:minmax(0,1fr)}
  .set-row.set-switch{grid-template-columns:minmax(0,1fr) auto}
  #lrPanel .lr-row{grid-template-columns:6rem minmax(0,1fr)}
  #lrPanel .lr-del{grid-column:1 / -1;justify-self:start}
}

/* ═══════════════════════════════════════════════════════════════════════
   v2 · States
   Empty screens with art, plan-locked features, the sidebar plan card.
   ═══════════════════════════════════════════════════════════════════════ */
/* ── The art frame ──
   Every state's art sits in one frame whose height is the same on every
   view, so an empty Tickets page, an empty Moderation log and a filter that
   found nothing put their title at the same place. A scene (3:2, opaque)
   fills it; a prop (a square cutout) sits centred in it. The warm pool behind
   both is the one decoration, drawn from brand tokens. --art-h is the knob:
   8rem in a view, 3rem inside a Dashboard panel, larger when the state is the
   whole page, smaller in the server picker. */
.art-frame{display:flex;align-items:center;justify-content:center;flex:none;
  block-size:min(var(--art-h, var(--space-32)), 30dvh);
  inline-size:min(100%, calc(var(--art-h, var(--space-32)) * 1.5));
  margin:0 auto var(--space-gap-lg);
  background:radial-gradient(closest-side, var(--color-brand-tint), transparent)}
.art-frame img{display:block;block-size:100%;inline-size:auto;max-inline-size:100%;margin:0;object-fit:contain}
.is-prop .art-frame img,.lock-card .art-frame img,.lock-note .art-frame img{aspect-ratio:1}
/* The scenes' near-black ground is lifted into the panel (and the warm pool)
   by the lighten blend described under "State illustrations" above; the
   props are true cutouts and must not blend, or their dark outlines would
   lift away with it. */
.is-scene .art-frame img{aspect-ratio:3 / 2;mix-blend-mode:lighten}
#shellState > .empty-art{--art-h:calc(var(--space-24) * 2)}
.srv-none .empty-art{--art-h:var(--space-28)}
@media (max-height:30rem){
  .art-frame{display:none}
}

/* ── Empty states ── */
.empty-art b{font-size:var(--text-base);font-weight:var(--weight-semibold);line-height:var(--leading-snug)}
.empty-art p{max-inline-size:44ch;margin-inline:auto;color:var(--color-text-secondary);line-height:var(--leading-normal);
  text-wrap:pretty}
/* The small form, inside a panel of the populated Dashboard overview. */
.empty-art.is-mini{--art-h:var(--space-12);padding:var(--space-6) var(--space-4)}
.empty-art.is-mini .art-frame{margin-bottom:var(--space-gap-md)}
.empty-art.is-mini b{font-size:var(--text-sm)}
.empty-art.is-mini p{font-size:var(--text-xs)}
/* Hosts that are not a plain block: the doors key row is a wrapping flex
   row, and the heatmap is an LTR grid that must not squeeze a sentence into
   its first cell or write Arabic left to right. */
.keys > .empty-art{flex:1 1 100%}
.heat.is-empty{display:block;min-width:0;direction:inherit}

/* ── Plan-locked: the upgrade card (lockCard) and its compact form ──
   The same surface as the sidebar plan card (warm gradient, brand hairline),
   so "this needs a plan" always looks like one thing. Centred like the empty
   states it replaces, with the benefits as a start-aligned list. */
.lock-wrap{padding-block:var(--space-gap-lg)}
.lock-card{display:flex;flex-direction:column;align-items:center;text-align:center;
  max-inline-size:32rem;margin-inline:auto;
  padding:var(--space-8) var(--space-card) var(--space-card);
  border:1px solid var(--color-interactive-selected-border);border-radius:var(--radius-xl);
  background:var(--gradient-brand-soft);box-shadow:var(--shadow-inner)}
.lk-body{display:flex;flex-direction:column;align-items:center;min-inline-size:0;inline-size:100%}
.lk-chip{margin-bottom:var(--space-gap-sm)}
.lk-title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);
  line-height:var(--leading-snug);color:var(--color-text-primary);text-wrap:balance}
.lk-sub{margin-top:var(--space-gap-xs);font-size:var(--text-sm);color:var(--color-text-secondary);
  line-height:var(--leading-normal);max-inline-size:40ch}
.lk-list{list-style:none;margin:var(--space-gap-lg) auto 0;padding:0;display:grid;gap:var(--space-gap-sm);
  text-align:start;inline-size:fit-content;max-inline-size:100%}
.lk-list li{display:flex;align-items:flex-start;gap:var(--space-gap-sm);font-size:var(--text-sm);
  color:var(--color-text-primary);line-height:var(--leading-normal)}
.lk-list .icon{flex:none;width:var(--size-icon-sm);height:var(--size-icon-sm);color:var(--color-text-brand);
  margin-top:calc((var(--text-sm) * var(--leading-normal) - var(--size-icon-sm)) / 2)}
.lk-acts{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;
  gap:var(--space-gap-sm) var(--space-gap-lg);margin-top:var(--space-gap-xl)}
.lk-link{display:inline-flex;align-items:center;gap:var(--space-gap-xs);min-height:var(--size-control-md);
  font-size:var(--text-sm)}
.lk-link .icon{width:var(--size-icon-sm);height:var(--size-icon-sm);flex:none}
.lk-cap{flex-basis:100%;margin:0;font-size:var(--text-xs);color:var(--color-text-tertiary)}
/* Compact, as a banner at the head of a settings card: art, words, actions. */
.lock-note{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:start;
  gap:var(--space-gap-lg);padding:var(--space-4);
  border:1px solid var(--color-interactive-selected-border);border-radius:var(--radius-lg);
  background:var(--gradient-brand-soft);box-shadow:var(--shadow-inner)}
.lock-note .art-frame{--art-h:var(--space-14);inline-size:var(--space-14);margin:0}
.lock-note .lk-body{align-items:flex-start;text-align:start}
.lock-note .lk-title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-gap-sm);font-size:var(--text-sm)}
.lock-note .lk-chip{margin:0}
.lock-note .lk-sub{max-inline-size:none;font-size:var(--text-xs)}
.lock-note .lk-acts{justify-content:flex-start;margin-top:var(--space-gap-md)}
/* The way out at the end of a callout (a full knowledge base's See plans) is
   a control: one line, and on touch the full target height, centred on the
   callout's first line so the words stay level with the sentence. As a flex
   item it shrank to its longest word, two lines in a box shorter than a
   fingertip. */
.callout > .link{flex:none;display:inline-flex;align-items:center;white-space:nowrap;
  min-height:calc(var(--text-sm) * var(--leading-relaxed))}
@media (max-width:52rem),(pointer:coarse){
  .callout > .link{min-height:var(--size-control-md);
    margin-block:calc((var(--text-sm) * var(--leading-relaxed) - var(--size-control-md)) / 2)}
}

/* ── The sidebar plan card (#upsell) ──
   A usage meter first (the number an owner on Free checks every day), then
   the offer and one action. Words are set in the interface face and numbers
   in tabular figures; nothing here is in the mono face, which has no Arabic
   and pulled the words of "0 / 50 رد" apart. */
.plan-card{display:flex;flex-direction:column;gap:var(--space-2);margin-bottom:var(--space-2);
  padding:var(--space-3);text-align:start;
  border:1px solid var(--color-interactive-selected-border);border-radius:var(--radius-lg);
  background:var(--gradient-brand-soft);box-shadow:var(--shadow-inner)}
.pc-head{display:flex;align-items:center;gap:var(--space-gap-sm);min-inline-size:0}
.pc-badge{display:inline-flex;align-items:center;justify-content:center;flex:none;
  inline-size:var(--size-avatar-sm);block-size:var(--size-avatar-sm);border-radius:var(--radius-full);
  background:var(--gradient-brand);color:var(--color-text-on-action)}
.pc-badge .icon{width:var(--size-icon-xs);height:var(--size-icon-xs)}
.pc-head b{font-family:var(--font-display);font-size:var(--text-sm);font-weight:var(--weight-semibold);
  color:var(--color-text-primary);line-height:var(--leading-snug)}
.pc-row{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-gap-sm);
  margin-top:var(--space-1)}
.pc-label{font-size:var(--text-xs);color:var(--color-text-secondary);min-inline-size:0}
.pc-val{font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--color-text-primary);
  white-space:nowrap;font-variant-numeric:tabular-nums}
.pc-meter{block-size:calc(var(--space-1) * 1.5);border-radius:var(--radius-full);
  background:var(--color-surface-page);overflow:hidden;margin-top:calc(var(--space-1) * -0.5)}
.pc-meter span{display:block;block-size:100%;inline-size:0;border-radius:var(--radius-full);
  background:var(--gradient-brand);transition:inline-size var(--duration-moderate) var(--ease-out)}
.plan-card.near .pc-meter span{background:var(--color-feedback-warning)}
.plan-card.over .pc-meter span{background:var(--color-feedback-error)}
.pc-note{font-size:var(--text-xs);color:var(--color-text-tertiary);line-height:var(--leading-normal)}
.plan-card.near .pc-note{color:var(--color-feedback-warning-text)}
.plan-card.over .pc-note{color:var(--color-feedback-error-text)}
.pc-sell{font-size:var(--text-xs);color:var(--color-text-secondary);line-height:var(--leading-normal);
  margin-top:var(--space-1);text-wrap:pretty}
.plan-card .btn{margin-top:var(--space-1)}
/* A neutral button, so the sidebar never shows a second solid-orange action
   beside the view's own; the brand is in its label instead. */
.plan-card #upCta{color:var(--color-text-brand)}
/* Every action a state offers is one control high (a fingertip), whatever
   size the call site asked for: the plan cards use regular buttons, and the
   small buttons the empty states were built with are brought up to match. */
.empty-art .btn-sm{min-height:var(--size-control-md);padding:0 var(--space-4);font-size:var(--text-sm);
  border-radius:var(--radius-button)}

/* ═══════════════════════════════════════════════════════════════════════
   v2 · Gating (owner, 2026-09-25: "the upgrade card is too big and lacks
   the details that catch the eye"). The market scan (scratchpad
   research/gating.md) found what sells without annoying: one small Pro
   pill used everywhere, the feature itself instead of a poster, the
   server's own numbers as the headline, and the offer as a strip with
   the price beside the button.
   ═══════════════════════════════════════════════════════════════════════ */
/* The Pro pill: the one gradient mark for everything Pro. No icon, no pulse. */
.pro-pill,.nav-item .badge.pro-pill{display:inline-flex;align-items:center;justify-content:center;
  min-block-size:var(--space-5);padding:0 var(--space-2);border-radius:var(--radius-pill);
  background:var(--gradient-brand);color:var(--color-text-on-action);font-family:var(--font-display);
  font-size:var(--text-xs);font-weight:var(--weight-bold);line-height:var(--leading-none);
  letter-spacing:normal;white-space:nowrap;vertical-align:middle;flex:none}
.pro-pill[hidden]{display:none}
/* A locked panel says so in its title. */
.panel.is-locked > .panel-head h2::after{content:"Pro";display:inline-flex;align-items:center;
  min-block-size:var(--space-5);padding:0 var(--space-2);margin-inline-start:var(--space-gap-sm);
  border-radius:var(--radius-pill);background:var(--gradient-brand);color:var(--color-text-on-action);
  font-family:var(--font-display);font-size:var(--text-xs);font-weight:var(--weight-bold);
  line-height:var(--leading-none);vertical-align:middle}

/* The offer strip: pill, headline, one outcome line; price, button, compare. */
.offer-strip{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;
  gap:var(--space-gap-md) var(--space-gap-xl);padding:var(--space-3) var(--space-4);
  border:1px solid var(--color-interactive-selected-border);border-radius:var(--radius-lg);
  background:var(--gradient-brand-soft)}
.os-copy{display:flex;align-items:flex-start;gap:var(--space-gap-md);flex:1 1 20rem;min-inline-size:0}
.os-copy b{display:block;color:var(--color-text-primary);font-size:var(--text-sm);
  font-weight:var(--weight-semibold);line-height:var(--leading-snug)}
.os-copy p{margin:var(--space-1) 0 0;font-size:var(--text-xs);color:var(--color-text-secondary);
  line-height:var(--leading-normal);max-inline-size:var(--size-prose-max)}
.os-copy .pro-pill{margin-block-start:var(--space-0)}
.offer-strip .lk-acts{margin:0;flex:0 1 auto;justify-content:flex-end;gap:var(--space-gap-sm) var(--space-gap-md)}
/* The price leads the actions in one line, not a caption under them. */
.offer-strip .lk-cap{flex-basis:auto;order:-1;font-size:var(--text-sm);color:var(--color-text-primary);
  font-weight:var(--weight-medium)}
.offer-strip .lk-cap .num{font-weight:var(--weight-bold)}
/* In a settings card it sits at the head, level with the rows' padding. */
.set-card > .lock-note.offer-strip{display:flex;margin-block:var(--space-card) 0;padding:var(--space-3) var(--space-4)}

/* Member health on Free: tiles with the server's counts, a glimpse of the
   real table, the strip. The whole thing is the panel's content. */
.peek{display:flex;flex-direction:column;gap:var(--space-gap-lg)}
.pk-tiles{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--space-gap-md)}
.pk-tile{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-gap-sm);
  padding:var(--space-3) var(--space-4);border:1px solid var(--color-border-default);
  border-radius:var(--radius-lg);background:var(--color-surface-raised);min-inline-size:0}
.pk-tile span{font-size:var(--text-xs);color:var(--color-text-secondary);min-inline-size:0}
.pk-tile b{font-family:var(--font-display);font-size:var(--text-2xl);line-height:var(--leading-none);
  color:var(--color-text-primary);font-variant-numeric:tabular-nums}
.pk-tile.bad b{color:var(--color-feedback-error)}
.pk-tile.warn b{color:var(--color-feedback-warning)}
.pk-tile.ok b{color:var(--color-feedback-success)}
.pk-preview{position:relative;overflow:hidden;border-radius:var(--radius-md);pointer-events:auto}
/* The last rows fade toward the strip below. */
.pk-preview::after{content:"";position:absolute;inset-inline:0;inset-block-end:0;block-size:55%;
  background:linear-gradient(to bottom,transparent,var(--color-card-bg));pointer-events:none}
.pk-preview .tbl td{padding-block:var(--space-2)}
.pk-av{inline-size:var(--size-avatar-sm);block-size:var(--size-avatar-sm);border-radius:var(--radius-full);
  background:var(--color-surface-raised);border:1px solid var(--color-border-default);flex:none}
.pk-bar{display:inline-block;block-size:var(--space-3);border-radius:var(--radius-full);
  background:var(--color-text-tertiary);opacity:var(--opacity-muted);filter:blur(var(--space-1))}
.pb-a{inline-size:7ch}.pb-b{inline-size:10ch}.pb-c{inline-size:6ch}.pb-d{inline-size:8ch}
.pb-n{inline-size:3ch}
.pk-tag{position:absolute;inset-block-start:var(--space-2);inset-inline-end:var(--space-2);z-index:1;
  font-size:var(--text-xs);padding:var(--space-1) var(--space-2);border-radius:var(--radius-pill);
  background:var(--color-surface-raised);border:1px solid var(--color-border-default);color:var(--color-text-secondary)}
/* One slow shimmer over the blurred rows on first view, never under reduced motion. */
@media (prefers-reduced-motion:no-preference){
  .pk-preview::before{content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
    background:linear-gradient(100deg,transparent 30%,var(--color-border-default) 50%,transparent 70%);
    transform:translateX(-100%);animation:pk-shine var(--duration-ambient) var(--ease-out) 1 both}
  [dir=rtl] .pk-preview::before{animation-name:pk-shine-rtl}
}
@keyframes pk-shine{to{transform:translateX(100%)}}
@keyframes pk-shine-rtl{from{transform:translateX(100%)}to{transform:translateX(-100%)}}
@media (max-width:40rem){
  .pk-tiles{gap:var(--space-gap-sm)}
  .pk-tile{flex-direction:column;align-items:flex-start;padding:var(--space-2) var(--space-3)}
  .pk-preview .pk-opt{display:none}
  .offer-strip .lk-acts{flex-basis:100%;justify-content:flex-start}
}

/* The welcome card on Free: a Preview button, and the card itself inside
   the settings row when it is opened. */
.tryon-btn .icon{width:var(--size-icon-sm);height:var(--size-icon-sm)}
.set-row > .tryon{grid-column:1 / -1}
.tryon{display:grid;grid-template-columns:minmax(0,18rem) minmax(0,1fr);align-items:start;
  gap:var(--space-gap-lg) var(--space-gap-xl);padding:var(--space-4);
  border:1px solid var(--color-interactive-selected-border);border-radius:var(--radius-lg);
  background:var(--gradient-brand-soft)}
.tryon[hidden]{display:none}
.tryon-art{position:relative;margin:0;border-radius:var(--radius-md);overflow:hidden;
  box-shadow:var(--shadow-lg);background:var(--color-surface-sunken)}
.tryon-art img{display:block;inline-size:100%;block-size:auto}
.tryon-ribbon{position:absolute;inset-block-start:var(--space-2);inset-inline-start:var(--space-2);
  font-size:var(--text-xs);font-weight:var(--weight-semibold);padding:var(--space-1) var(--space-2);
  border-radius:var(--radius-pill);background:var(--color-surface-overlay);color:var(--color-text-primary)}
.tryon-body{display:flex;flex-direction:column;gap:var(--space-2);min-inline-size:0}
.tryon-body p{margin:0;font-size:var(--text-xs);color:var(--color-text-secondary);line-height:var(--leading-normal)}
.tryon-cap{color:var(--color-text-tertiary)}
.tryon-title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-gap-sm);
  font-size:var(--text-sm);color:var(--color-text-primary);line-height:var(--leading-snug)}
.tryon-title:focus{outline:none}
.tryon-title:focus-visible{outline:var(--width-focus) solid var(--color-border-focus);outline-offset:var(--width-focus)}
.tryon-own{color:var(--color-text-primary)}
.tryon-acts{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--space-gap-sm)}
.tryon-acts .lk-acts{margin:0;justify-content:flex-start}
.tryon .lk-cap{flex-basis:100%}
@container (max-width:40rem){
  .tryon{grid-template-columns:minmax(0,1fr)}
}

/* ═══════════════════════════════════════════════════════════════════════
   v2 · How the bot looks in a server (branding.py, migration 031): the
   bot's profile and one of its messages the way members meet them, the
   picture and banner rows, and the welcome card's backgrounds with the
   real card drawn under them.
   ═══════════════════════════════════════════════════════════════════════ */
.set-head h2 .pro-pill{margin-inline-start:var(--space-gap-sm)}
.set-card > #lookLock{margin-block:var(--space-card) 0}
.set-card > #lookErr{margin-block:var(--space-card) 0}
.set-card > #lookLock + #lookErr[hidden] + .set-row,.set-card > #lookErr + .set-row{border-top:0}
/* The profile card beside the member list and a message: the three places
   members meet the bot, each captioned in the console's own words. */
.look-preview{display:grid;grid-template-columns:minmax(0,17rem) minmax(0,1fr);align-items:start;
  gap:var(--space-gap-lg);margin-block-start:var(--space-gap-md)}
.lp-fig,.lp-col{display:flex;flex-direction:column;min-inline-size:0}
.lp-fig{gap:var(--space-2)}
.lp-col{gap:var(--space-gap-lg)}
.lp-cap{font-size:var(--text-xs);color:var(--color-text-tertiary);line-height:var(--leading-snug)}
.lp-list{display:flex;align-items:center;gap:var(--space-gap-md);padding:var(--space-2) var(--space-4);
  border:1px solid var(--color-border-default);border-radius:var(--radius-lg);
  background:var(--color-surface-raised);min-inline-size:0}
.lp-list-face{flex:none;inline-size:var(--size-avatar-md);block-size:var(--size-avatar-md);
  border-radius:var(--radius-full);object-fit:cover;background:var(--color-surface-sunken)}
.lp-profile{border:1px solid var(--color-border-default);border-radius:var(--radius-lg);overflow:hidden;
  background:var(--color-surface-raised);min-inline-size:0}
.lp-banner{aspect-ratio:5 / 2;max-inline-size:100%;background:var(--gradient-brand-soft)}
.lp-banner img{display:block;inline-size:100%;block-size:100%;object-fit:cover}
.lp-face{display:block;inline-size:var(--size-avatar-xl);block-size:var(--size-avatar-xl);
  margin:calc(var(--size-avatar-xl) / -2) 0 0 var(--space-4);border-radius:var(--radius-full);
  object-fit:cover;background:var(--color-surface-sunken);
  box-shadow:0 0 0 var(--space-1) var(--color-surface-raised)}
.lp-body{display:flex;flex-direction:column;gap:var(--space-1);padding:var(--space-2) var(--space-4) var(--space-4)}
.lp-name{display:flex;align-items:center;gap:var(--space-gap-xs);margin:0;min-inline-size:0}
.lp-name b{font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--color-text-primary);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-inline-size:0}
.lp-app{flex:none;padding:0 var(--space-1);border-radius:var(--radius-sm);background:var(--color-text-link);
  color:var(--color-text-on-action);font-size:var(--text-xs);font-weight:var(--weight-semibold);
  line-height:var(--leading-snug)}
.lp-bio{margin:0;font-size:var(--text-xs);color:var(--color-text-secondary);line-height:var(--leading-normal);
  white-space:pre-line;overflow-wrap:anywhere}
.lp-msg{display:flex;align-items:flex-start;gap:var(--space-gap-md);padding:var(--space-3) var(--space-4);
  border:1px solid var(--color-border-default);border-radius:var(--radius-lg);background:var(--color-surface-raised);
  min-inline-size:0}
.lp-msg-face{flex:none;inline-size:var(--size-avatar-lg);block-size:var(--size-avatar-lg);
  border-radius:var(--radius-full);object-fit:cover;background:var(--color-surface-sunken)}
.lp-msg-body{display:flex;flex-direction:column;gap:var(--space-1);min-inline-size:0}
.lp-text{margin:0;font-size:var(--text-sm);color:var(--color-text-primary);line-height:var(--leading-normal)}
/* Free: the bio is on screen but inert (the picture and banner buttons are
   greyed by .btn:disabled), so it offers no resize grip either. */
#lookSec textarea:disabled{resize:none}
/* Upload and Remove sit at the row's end, one control high. */
.brand-ctl{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:var(--space-gap-sm)}
.brand-ctl .btn{min-block-size:var(--size-control-md)}
.brand-ctl .btn .icon{width:var(--size-icon-sm);height:var(--size-icon-sm)}
.brand-ctl .btn[aria-busy=true]{opacity:var(--opacity-loading);cursor:progress}
/* What happened to the last upload, under the row's label. */
.brand-state{display:flex;align-items:flex-start;gap:var(--space-gap-xs);margin:var(--space-2) 0 0;
  font-size:var(--text-xs);line-height:var(--leading-normal);color:var(--color-text-secondary);
  max-inline-size:var(--size-prose-max)}
.brand-state[hidden]{display:none}
.brand-state::before{content:"";flex:none;inline-size:var(--space-2);block-size:var(--space-2);
  margin-block-start:calc((1lh - var(--space-2)) / 2);border-radius:var(--radius-full);
  background:var(--color-text-tertiary)}
.brand-state.ok::before{background:var(--color-feedback-success)}
.brand-state.wait::before{background:var(--color-feedback-warning)}
.brand-state.bad{color:var(--color-feedback-error-text)}
.brand-state.bad::before{background:var(--color-feedback-error)}
/* The card's backgrounds: one tile per scene and one for the server's own
   image, as radio buttons, with the card itself underneath. */
.wb{display:flex;flex-direction:column;gap:var(--space-gap-md);min-inline-size:0}
/* Six tiles (five scenes and the server's own): six across, then three or
   two, so no row is left with one tile on its own. */
.wb-tiles{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:var(--space-gap-md)}
.wb-tile{position:relative;display:flex;flex-direction:column;gap:var(--space-1);min-inline-size:0;cursor:pointer}
.wb-tile input{position:absolute;opacity:0;inline-size:1px;block-size:1px;pointer-events:none}
.wb-face{position:relative;display:grid;place-items:center;aspect-ratio:20 / 9;max-inline-size:100%;
  overflow:hidden;border-radius:var(--radius-md);border:1px solid var(--color-border-default);
  background:var(--color-surface-sunken);color:var(--color-text-tertiary)}
.wb-face img{display:block;inline-size:100%;block-size:100%;object-fit:cover}
.wb-face .icon{width:var(--size-icon-md);height:var(--size-icon-md)}
.wb-tile:hover .wb-face{border-color:var(--color-border-strong)}
.wb-tile input:checked + .wb-face{border-color:var(--color-interactive-selected-border-strong);
  box-shadow:0 0 0 var(--width-focus) var(--color-interactive-selected-border-strong)}
.wb-tile input:focus-visible + .wb-face{outline:var(--width-focus) solid var(--color-border-focus);
  outline-offset:var(--width-focus)}
.wb-tile input:disabled + .wb-face{cursor:default}
.wb-tile.is-add .wb-face{border-style:dashed}
.wb-tile:disabled{cursor:default}
.wb-tile:disabled .wb-face{border-style:solid;opacity:var(--opacity-muted)}
.wb-name{display:flex;align-items:center;gap:var(--space-gap-xs);font-size:var(--text-xs);
  color:var(--color-text-secondary);line-height:var(--leading-snug);min-inline-size:0}
.wb-tile input:checked ~ .wb-name{color:var(--color-text-primary);font-weight:var(--weight-semibold)}
.wb-badge{position:absolute;inset-block-start:var(--space-1);inset-inline-start:var(--space-1);
  padding:0 var(--space-1);border-radius:var(--radius-pill);font-size:var(--text-xs);
  line-height:var(--leading-snug);background:var(--color-surface-overlay);color:var(--color-text-primary)}
.wb-badge.bad{color:var(--color-feedback-error-text)}
.wb-foot{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--space-gap-sm)}
.wb-foot .brand-state{margin:0;flex:1 1 16rem}
.wb-acts{display:flex;flex-wrap:wrap;gap:var(--space-gap-sm)}
.wb-acts .btn{min-block-size:var(--size-control-md)}
.wb-preview{margin:0;display:flex;flex-direction:column;gap:var(--space-2);max-inline-size:36rem}
.wb-preview img{display:block;inline-size:100%;block-size:auto;aspect-ratio:20 / 9;max-inline-size:100%;
  border-radius:var(--radius-md);background:var(--color-surface-sunken);box-shadow:var(--shadow-md)}
.wb-preview img[aria-busy=true]{opacity:var(--opacity-loading)}
.wb-preview figcaption{font-size:var(--text-xs);color:var(--color-text-tertiary);line-height:var(--leading-normal)}
#lookErr p{margin:0;line-height:var(--leading-relaxed)}
@container (max-width:48rem){
  .wb-tiles{grid-template-columns:repeat(3,minmax(0,1fr))}
}
@container (max-width:40rem){
  .look-preview{grid-template-columns:minmax(0,1fr)}
  .brand-ctl{justify-content:flex-start}
}
@container (max-width:26rem){
  .wb-tiles{grid-template-columns:repeat(2,minmax(0,1fr))}
}

/* The operator's side of branding (admin console): the review queue above
   the servers table, and the drawer's Branding section. */
.panel > .brand-queue{margin:0 0 var(--space-gap-lg)}
.brand-queue p{margin:0;line-height:var(--leading-relaxed)}
.brand-queue-list{margin:var(--space-2) 0 0;padding:0;list-style:none;display:flex;flex-direction:column;
  gap:var(--space-1);font-size:var(--text-sm);color:var(--color-text-secondary)}
.brand-queue-list .link{font-size:var(--text-sm);background:none;border:0;padding:0;cursor:pointer}
.brand-queue-list .sub{color:var(--color-text-tertiary);font-size:var(--text-xs)}
.brand-item{display:grid;grid-template-columns:7rem minmax(0,1fr);gap:var(--space-gap-md);align-items:start;
  padding-block:var(--space-3);border-bottom:1px solid var(--color-border-default)}
.brand-thumb{display:block;border-radius:var(--radius-md);overflow:hidden;border:1px solid var(--color-border-default);
  background:var(--color-surface-sunken)}
.brand-thumb img{display:block;inline-size:100%;block-size:auto;object-fit:cover;max-inline-size:100%}
.brand-thumb.brand-avatar{inline-size:var(--size-avatar-xl);border-radius:var(--radius-full)}
.brand-thumb.brand-avatar img{aspect-ratio:1 / 1}
.brand-thumb.brand-banner img{aspect-ratio:5 / 2}
.brand-thumb.brand-welcome img{aspect-ratio:20 / 9}
.brand-meta{display:flex;flex-direction:column;gap:var(--space-1);min-inline-size:0}
.brand-meta p{margin:0;font-size:var(--text-sm)}
.brand-meta .sub{color:var(--color-text-tertiary);font-size:var(--text-xs)}
.brand-acts{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-gap-sm);margin-block-start:var(--space-1)}
.brand-reject{display:flex;align-items:center;gap:var(--space-gap-sm);min-inline-size:0}
.brand-reject .btn{flex:none}
.brand-reason{flex:1 1 auto;min-inline-size:0;min-block-size:var(--size-control-sm);font-size:var(--text-xs);
  border:1px solid var(--color-border-control);border-radius:var(--radius-input);background:var(--color-surface-page);
  color:var(--color-text-primary);padding-inline:var(--space-2)}
.check.brand-block{margin:var(--space-3) 0 0}
