/* =============================================================================
   TG Access — tenant dashboard
   =============================================================================

   The same visual language as the owner control centre (admin.css), built for
   the other audience: a channel owner who is not technical, on a phone as often
   as a laptop. Same shell, same cards, same chips and avatars, same empty
   states, same toasts; warmer accents, bigger touch targets, fewer numbers per
   screen.

   SELF-SUFFICIENT ON PURPOSE. The shell links tokens.css and this file, and
   nothing else. It used to link tgaccess.css as well — *after* this file — so
   the public site's `.btn`, `.card` and `.chip` silently won every collision
   and half of the rules below never applied. A working surface cannot share a
   marketing stylesheet's cascade. What the dashboard genuinely needs from the
   site (the guide notice, the brand logo) is either in tokens.css already or
   restated at the bottom of this file.

   THEMING. Both themes are defined as tokens on :root; `[data-theme]` on <html>
   picks between them and dashboard.js remembers the choice. With no choice
   stored, prefers-color-scheme decides. Every colour below is a token, because
   a hardcoded hex survives the theme switch and looks broken in one of the two.

   RTL. Logical properties throughout (inline-start, margin-inline, inset-inline,
   border-inline-end) so Arabic mirrors with no directional override anywhere.
   ========================================================================== */

:root {
  /* ---- light (default) ---- */
  --dash-bg:         #f4f6fb;
  --dash-panel:      #ffffff;
  --dash-panel-2:    #fbfcfe;
  --dash-ink:        #0f1b4c;
  --dash-ink-muted:  #5a6486;
  /* Measured, not eyeballed (D-274): the old #8b93ad was 3.05:1 on white —
     table headers, tile labels, "no comparison yet" — nowhere near the
     4.5:1 body-text floor. Darkened just enough to clear it while staying
     visibly lighter than --dash-ink-muted. */
  /* Dark enough for a TINTED ground, not only for pure white. At #677390 it measured
     4.74:1 on white and 4.38:1 on --dash-panel-2, so the same muted paragraph passed on
     one card and failed on the next — which is how D-274 ended up fixing ink-faint on
     line-soft twice, locally, instead of once here. #606a85 is 4.98:1 on the tinted
     panel and 4.77:1 on line-soft, so every ground the dashboard actually paints clears
     AA with the hierarchy intact. */
  --dash-ink-faint:  #606a85;
  --dash-line:       #e4e8f2;
  --dash-line-soft:  #eef1f7;
  --dash-field:      #ffffff;

  --dash-side-bg:    #0d1533;
  --dash-side-bg-2:  #131d45;
  --dash-side-ink:   #c9d2ee;
  --dash-side-dim:   #7d88b0;
  --dash-side-line:  rgba(255,255,255,.08);

  --dash-brand:      #2f5bd7;
  --dash-brand-soft: #eaf0ff;
  --dash-accent:     #6e42c1;
  --dash-accent-soft:#f2ecff;
  --dash-gold:       #ffc93d;
  --dash-gold-soft:  #fff6dd;
  --dash-on-gold:    #3a2a00;
  --dash-white:      #fff;
  --dash-on-brand:   #fff;
  --dash-qr-ink:     #0f1b4c;
  --dash-side-urgent-ink: #ffc9c9;
  --dash-side-urgent-dot: #ff6b6b;

  /* D-274: --dash-ok/--dash-warn/--dash-bad darkened from their original
     #16a34a / #d97706 / #dc2626. Every one of the three is also used as
     TEXT on its own "-soft" tint (a chip, an alert, the finished checklist
     row) and the originals measured 2.9-4.2:1 there — below 4.5:1 and, for
     two of the three, below even the old pre-token design's own intent.
     Darkening the shared value fixes that pairing in light mode and only
     IMPROVES the other pairing these tokens are used in (white text on a
     solid button), so nothing that was passing stops passing. */
  --dash-ok:         #166534;
  --dash-ok-soft:    #e7f7ed;
  --dash-warn:       #92400e;
  --dash-warn-soft:  #fef3e2;
  --dash-bad:        #b91c1c;
  --dash-bad-soft:   #fdeaea;

  /* The "-ink" siblings (D-274): the role these four colours play when they
     ARE the text, sitting on their own soft tint or on a plain panel,
     rather than being a background under white text. Light mode's ink is
     simply the colour above -- already dark enough. Dark mode needs its
     OWN, much brighter value for the same role, because "ok" as a
     background with white text wants a dark, saturated green and "ok" as
     text on a near-black tint wants the opposite -- one token cannot be
     both, which is exactly how D-263's shape came back here. */
  --dash-ok-ink:     var(--dash-ok);
  --dash-warn-ink:   var(--dash-warn);
  --dash-bad-ink:    var(--dash-bad);
  --dash-brand-ink:  var(--dash-brand);
  --dash-accent-ink: var(--dash-accent);

  --dash-grad:       linear-gradient(120deg, #2f5bd7, #6e42c1);
  --dash-grad-gold:  linear-gradient(120deg, #ffc93d, #ffa72b);
  --dash-tile-grad-green: linear-gradient(120deg, #16a34a, #4ade80);
  --dash-tile-grad-red:   linear-gradient(120deg, #dc2626, #f87171);
  --dash-avatar-grad-1: linear-gradient(135deg,#2f5bd7,#4f86ff);
  --dash-avatar-grad-2: linear-gradient(135deg,#6e42c1,#a06bff);
  --dash-avatar-grad-3: linear-gradient(135deg,#e0a21b,#ffc93d);
  --dash-avatar-grad-4: linear-gradient(135deg,#0e9f6e,#34d399);
  --dash-avatar-grad-5: linear-gradient(135deg,#d64545,#ff7a7a);

  --dash-shadow:     0 1px 2px rgba(15,27,76,.05), 0 8px 24px rgba(15,27,76,.06);
  --dash-shadow-lg:  0 20px 60px rgba(15,27,76,.18);
  --dash-radius:     16px;
  --dash-radius-sm:  10px;
  --dash-speed:      .22s;
  --dash-font:       Tajawal, "Tajawal Persian", "Tajawal Hindi", system-ui, -apple-system, "Segoe UI", sans-serif;
}

:root[data-theme="dark"] {
  --dash-bg:         #0a1028;
  --dash-panel:      #111a3d;
  --dash-panel-2:    #16214a;
  --dash-ink:        #eef2ff;
  --dash-ink-muted:  #a3aed4;
  /* D-274: #6f7ba3 measured 4.06:1 against the dark panel it sits on in
     table headers and "sub" captions -- under the 4.5:1 floor by enough
     that it was not a rounding question. */
  --dash-ink-faint:  #8691b8;
  --dash-line:       #222e5c;
  --dash-line-soft:  #1b2650;
  --dash-field:      #0d1533;

  --dash-side-bg:    #070c1f;
  --dash-side-bg-2:  #0d1533;
  --dash-side-ink:   #b9c3e6;

  --dash-brand-soft: #16224e;
  --dash-accent-soft:#1e1a45;
  --dash-gold-soft:  #2e2410;
  --dash-ok-soft:    #0f2a1c;
  --dash-warn-soft:  #2e2110;
  --dash-bad-soft:   #2e1417;

  /* D-274: the dark-mode "-ink" values. --dash-ok/--dash-warn/--dash-bad/
     --dash-brand themselves stay the SAME dark, saturated colour in both
     themes -- that is what a solid button background with white text
     wants, in light or dark -- so the text-on-tint role needs its own,
     much brighter value here instead of inheriting the (now even darker)
     base. Measured against this theme's own "-soft" tint and against
     --dash-panel-2, each with room above 4.5:1. */
  --dash-ok-ink:     #4ade80;
  --dash-warn-ink:   #fbbf24;
  --dash-bad-ink:    #f87171;
  --dash-brand-ink:  #7aa4ff;
  --dash-accent-ink: #b088ff;

  --dash-shadow:     0 1px 2px rgba(0,0,0,.3), 0 8px 24px rgba(0,0,0,.35);
  --dash-shadow-lg:  0 24px 70px rgba(0,0,0,.6);
}

/* No explicit choice stored: follow the system. Guarded with the :not()s so an
   explicit light choice is not overridden by a dark OS setting. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="dark"]) {
    --dash-bg: #0a1028;        --dash-panel: #111a3d;    --dash-panel-2: #16214a;
    --dash-ink: #eef2ff;       --dash-ink-muted: #a3aed4;
    --dash-ink-faint: #8691b8; --dash-line: #222e5c;     --dash-line-soft: #1b2650;
    --dash-field: #0d1533;
    --dash-side-bg: #070c1f;   --dash-side-bg-2: #0d1533;
    --dash-side-ink: #b9c3e6;
    --dash-brand-soft: #16224e; --dash-accent-soft: #1e1a45;
    --dash-gold-soft: #2e2410;  --dash-ok-soft: #0f2a1c;
    --dash-warn-soft: #2e2110;  --dash-bad-soft: #2e1417;
    --dash-ok-ink: #4ade80;    --dash-warn-ink: #fbbf24;
    --dash-bad-ink: #f87171;   --dash-brand-ink: #7aa4ff;
    --dash-accent-ink: #b088ff;
    --dash-shadow: 0 1px 2px rgba(0,0,0,.3), 0 8px 24px rgba(0,0,0,.35);
    --dash-shadow-lg: 0 24px 70px rgba(0,0,0,.6);
  }
}

/* ============================================================ base + shell */

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

body.dash {
  margin: 0;
  background: var(--dash-bg);
  color: var(--dash-ink);
  font-family: var(--dash-font);
  font-size: var(--text-base);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; }

.visually-hidden {
  position: absolute;
  inline-size: 1px; block-size: 1px;
  margin: calc(-1 * var(--space-px)); padding: 0;
  overflow: hidden; clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

.dash-shell {
  display: grid;
  grid-template-columns: 258px minmax(0, 1fr);
  min-block-size: 100vh;
}

/* ---------------------------------------------------------------- sidebar */

.dash-side {
  background: linear-gradient(180deg, var(--dash-side-bg), var(--dash-side-bg-2));
  color: var(--dash-side-ink);
  padding: var(--space-6) var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  position: sticky;
  inset-block-start: 0;
  block-size: 100vh;
  overflow-y: auto;
  border-inline-end: 1px solid var(--dash-side-line);
}

.dash-side .brand { margin-block-end: 20px; padding-inline: var(--space-2); }

.dash-side-label {
  font-size: var(--text-xs);
  font-weight: 800;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--dash-side-dim);
  padding: var(--space-4) var(--space-3) var(--space-2);
}

.side-link {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-3);
  border-radius: var(--dash-radius-sm);
  color: var(--dash-side-ink);
  text-decoration: none;
  font-weight: 600;
  position: relative;
  transition: background var(--dash-speed), color var(--dash-speed),
              transform var(--dash-speed);
}
.side-link:hover { background: rgba(255,255,255,.06); color: var(--dash-on-brand); }

/* The active item takes the brand gradient and a gold rail on the inline
   start — a logical edge, so it mirrors correctly in Arabic. */
.side-link.is-active {
  background: var(--dash-grad);
  color: var(--dash-on-brand);
  box-shadow: 0 6px 18px rgba(47,91,215,.4);
}
.side-link.is-active::before {
  content: "";
  position: absolute;
  inset-inline-start: -16px;
  inset-block: 8px;
  inline-size: 3px;
  /* Token-exempt: see the design-token-usage test for why these two
     geometry-driven corner radii are allowed to stay raw px. */
  border-radius: 0 3px 3px 0;
  background: var(--dash-gold);
}

.side-link svg { inline-size: 19px; block-size: 19px; flex: none;
                 transition: transform var(--dash-speed); }
.side-link:hover svg { transform: translateY(-1px) scale(1.06); }
.side-link.is-active svg { animation: side-pop .32s ease; }

@keyframes side-pop {
  0%   { transform: scale(.82); }
  60%  { transform: scale(1.12); }
  100% { transform: scale(1); }
}

/* A count, never a zero: a badge showing 0 teaches the eye to ignore badges. */
.side-badge {
  margin-inline-start: auto;
  min-inline-size: 21px;
  padding: var(--space-px) var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--dash-bad);
  color: var(--dash-on-brand);
  font-size: var(--text-xs);
  font-weight: 800;
  text-align: center;
  animation: badge-pulse 2.4s ease-in-out infinite;
}
.side-link.is-active .side-badge { background: rgba(255,255,255,.26); }

/* "Ready" replaces the count when nothing is outstanding, so it must not keep the
   alarm colour: red says something is wrong, and nothing is. Wider than the count
   badge because it holds a word, and it does not pulse -- the pulse is what draws
   the eye to a problem (D-266). */
.side-badge.is-ready {
  background: var(--dash-ok);
  padding-inline: var(--space-2);
  font-size: var(--text-xs);
  letter-spacing: .01em;
  animation: none;
}

@keyframes badge-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.08); }
}

.dash-side-foot { margin-block-start: auto; padding-block-start: var(--space-5); }
.dash-side-foot form { margin: 0; }
.dash-side-foot .side-link {
  inline-size: 100%;
  border: 0;
  background: none;
  cursor: pointer;
  font: inherit;
  font-weight: 600;
  text-align: start;
}

/* Two links sharing one row's height budget — see the comment in _base.html for why. */
.side-link-row { display: flex; align-items: center; gap: var(--space-1); }
.side-link-row .side-link { inline-size: auto; flex: 1; min-inline-size: 0; }
.side-link-row .side-icon-only {
  flex: none;
  justify-content: center;
  padding-inline: var(--space-3);
  /* A 44px floor in both directions, matched explicitly rather than trusted to padding
     math — the same reasoning every other tap target in this file uses: a thumb is not
     a mouse pointer, and this one measured 43x44 on a phone before the floor was explicit. */
  min-inline-size: 44px;
  min-block-size: 44px;
}

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

.dash-main { min-inline-size: 0; display: flex; flex-direction: column; }

.dash-top {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-4) var(--space-6);
  background: var(--dash-panel);
  border-block-end: 1px solid var(--dash-line);
  position: sticky;
  inset-block-start: 0;
  z-index: 40;
}
.dash-top h1 { margin: 0; font-size: var(--text-xl); font-weight: 800; letter-spacing: -.01em; }
.dash-top .spacer { flex: 1; }
/* The switch page puts the logo where the pages inside the shell put the menu button,
   so its heading needs the gap the shell's own padding would otherwise have given it. */
.title-beside-logo { margin-inline-start: var(--space-4); }

.dash-body { padding: var(--space-6) var(--space-6) var(--space-16); }

/* Page-level intro copy sitting above the first card, with no card of its
   own around it — `.note` is a boxed hint INSIDE a card and `.checklist
   .lede` only applies inside the checklist, so a page lede (the setup
   wizard's "here is the whole wizard" sentence) needed the same quiet
   treatment without either ancestor. */
/* A card's own opening sentence. This had NO bare definition: the only `.lede` rules
   were `.checklist .lede` and `.pay-panel .lede`, so the same paragraph was styled on
   two pages and unstyled on `tier.html` and `reconnect.html` — and the class gate could
   not see it, because a name used in any selector counts as defined. One definition
   (D-207), with those two keeping their refinements. */
.lede {
  margin: 0 0 var(--space-3);
  font-size: var(--text-sm);
  line-height: var(--leading);
  color: var(--dash-ink-muted);
}

.page-lede {
  margin: 0 0 var(--space-4);
  font-size: var(--text-sm);
  line-height: var(--leading);
  color: var(--dash-ink-muted);
}

/* The dashboard home's greeting line and its live-status badge (D-361). */
.dash-greeting {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
}
.dash-live {
  display: inline-flex; align-items: center; gap: var(--space-2);
  color: var(--dash-ok-ink); font-size: var(--text-sm);
}
.dash-live-dot {
  inline-size: 8px; block-size: 8px; border-radius: 50%;
  background: var(--dash-ok);
}

/* The "needs your attention" card (D-363): one link per row, each a full-width tap
   target (≥44px, per the dashboard's own phone gate) rather than an inline link
   lost in a paragraph. */
.attn-list { list-style: none; margin: 0; padding: 0; }
.attn-list li + li { border-block-start: 1px solid var(--dash-line); }
.attn-list a {
  display: flex; align-items: center;
  min-block-size: 44px; padding-block: var(--space-3);
  color: var(--dash-ink); text-decoration: none;
}
.attn-list a:hover, .attn-list a:focus-visible { color: var(--dash-brand); }

/* ------------------------------------------------------ the account menu */

/* Opened with the native Popover API, so no script and no inline handler is
   needed — which is the only way a menu can exist under a CSP with no
   'unsafe-inline' on script-src. */
.dash-who {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3) var(--space-1) var(--space-2);
  border: 1px solid var(--dash-line);
  border-radius: var(--radius-pill);
  background: var(--dash-panel);
  color: var(--dash-ink);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 700;
  cursor: pointer;
  transition: border-color var(--dash-speed), box-shadow var(--dash-speed);
}
.dash-who:hover { border-color: var(--dash-brand); box-shadow: var(--dash-shadow); }
.dash-who .name { max-inline-size: 11ch; overflow: hidden;
                  text-overflow: ellipsis; white-space: nowrap; }
.dash-who svg { inline-size: 13px; block-size: 13px; color: var(--dash-ink-faint); }

/* `display` is declared ONLY on the open state, and this is not a style
   preference. The UA hides a closed popover with
   `[popover]:not(:popover-open) { display: none }` — a UA rule, which ANY
   author `display` declaration outranks. Writing `display: grid` on the
   element itself therefore pins the menu open forever, on every page, covering
   whatever is under it. Found in a screenshot; it had shipped.

   Declared this way round on purpose: in a browser with no popover support
   `:popover-open` is an invalid selector and the rule is dropped, so the menu
   falls back to always visible rather than never reachable. */
/* `[popover].account-pop`, not `.account-pop`: the generic `[popover]` rule far
   below sets `padding: var(--space-4)`, it has the SAME specificity as a single class,
   and it comes later in the file — so the account menu rendered at the confirm
   dialog's padding and this var(--space-2) never applied. Qualifying the selector with the
   attribute takes it to 0-2-0 and makes the outcome independent of which rule
   moves in the file. */
[popover].account-pop {
  min-inline-size: 234px;
  /* Left as the literal 8px, not var(--space-2) (the two are the same
     value): test_dashboard_csp.py's
     test_the_account_menu_really_gets_its_own_padding asserts this exact
     declaration text, named after the specificity regression in the
     comment above, and a .py file is not this change's to edit.

     D-379 changed this to var(--space-2) anyway, missing the comment it was
     sitting right above — exactly the regression predicted. Reverted; this
     one declaration (and its gap, for the same 2px-not-1px reason) stays a
     documented exemption from that scale rather than a second attempt to
     make the test agree with the token. */
  padding: 8px;
  gap: 2px;
}
.account-pop:popover-open { display: grid; }
.account-pop .who-head {
  padding: var(--space-2) var(--space-3) var(--space-3);
  border-block-end: 1px solid var(--dash-line-soft);
  margin-block-end: 6px;
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.account-pop .who-head b { display: block; font-size: var(--text-base); }
.account-pop .who-head small { color: var(--dash-ink-faint); font-size: var(--text-xs); }
.account-pop a,
.account-pop button {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--dash-radius-sm);
  background: none;
  color: inherit;
  font: inherit;
  font-size: var(--text-base);
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  inline-size: 100%;
  text-align: start;
}
.account-pop a:hover,
.account-pop button:hover { background: var(--dash-brand-soft); color: var(--dash-brand-ink); }
.account-pop form { margin: 0; }
.account-pop svg { inline-size: 15px; block-size: 15px; flex: none; }
.account-pop .is-danger:hover { background: var(--dash-bad-soft); color: var(--dash-bad-ink); }

/* ---------------------------------------------------- language + theme seg */

.seg {
  display: inline-flex;
  background: var(--dash-panel-2);
  border: 1px solid var(--dash-line);
  border-radius: var(--radius-sm);
  padding: var(--space-px);
  gap: var(--space-px);
}
.seg a, .seg button {
  padding: var(--space-1) var(--space-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--dash-ink-muted);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
}
.seg a.is-on, .seg button.is-on {
  background: var(--dash-panel);
  color: var(--dash-ink);
  box-shadow: var(--dash-shadow);
}

/* The top bar is sticky, so anything the browser scrolls into view — a
   keyboard tab, an in-page #anchor, a script clicking a control inside a
   horizontally scrolling table — lands underneath it and cannot be clicked.
   A scroll margin the height of the bar is the whole fix, and it is a logical
   property so it is correct in both writing directions. */
.dash-body :is(a, button, input, select, textarea, [popovertarget], [id]) {
  scroll-margin-block-start: 84px;
}

/* ============================================================ cards + grid */

.dash-grid { display: grid; gap: var(--space-4); }
.dash-grid-2 { grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
               align-items: start; }
.dash-grid-even { grid-template-columns: repeat(auto-fit, minmax(330px, 1fr));
                  align-items: start; }
/* `align-items: stretch`, which is the grid default, and the reason this rule is
   written out rather than left off: it used to say `start`, so two payment cards
   with different amounts of content had different heights and their Save buttons
   sat at different places on the row. The owner's screenshot shows three cards of
   three heights (2026-10-04). Every card in this grid is a panel with a header and
   a body — they belong on one baseline. */
.dash-grid-cards { grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
                   align-items: stretch; }

/* And the content fills the stretched card, so the action at the bottom of a short
   card lines up with the action at the bottom of a tall one instead of floating
   under its own text. */
.dash-grid-cards > .card { display: flex; flex-direction: column; }
.dash-grid-cards > .card > .pad { flex: 1; display: flex; flex-direction: column; }
.dash-grid-cards > .card > .pad > form:last-child,
.dash-grid-cards > .card > .pad > p:last-child { margin-block-start: auto; }

.card {
  background: var(--dash-panel);
  border: 1px solid var(--dash-line);
  border-radius: var(--dash-radius);
  box-shadow: var(--dash-shadow);
  overflow: hidden;
}
/* Two cards stacked outside a `.dash-grid` (one list, then one card per row of
   it; several stand-alone sections one under another) touched edge to edge
   with nothing of their own to say they were two sections and not one split
   by a stray border. The adjacent-sibling selector gives them the grid's own
   gap without caring whether the sibling in between is there — a conditional
   section that sometimes does not render leaves the spacing correct either
   way, which a class on "the second one" cannot. */
.card + .card { margin-block-start: var(--space-4); }
.card > header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-4) var(--space-5);
  border-block-end: 1px solid var(--dash-line);
}
.card > header h2 { margin: 0; font-size: var(--text-md); font-weight: 800; }
.card > header .spacer { flex: 1; }
.card .pad { padding: var(--space-5); }

/* A `.pad` whose bottom edge sits against something else inside the same
   card — a divider, a second form — rather than the card's own border. The
   two live beside `.pad` rather than replacing it, so a template keeps the
   normal padding on every edge it does not need to change. */
.pad-tight-end { padding-block-end: var(--space-1); }
.pad-divided   { border-block-start: 1px solid var(--dash-line); }

/* A full-width block stacked under a `.dash-grid` (or another card), outside
   the grid itself — the grid's own `gap` does not reach a sibling sitting
   next to it, so this is the same 16px rhythm for that one case. */
.stack-top { margin-block-start: var(--space-4); }

/* The note that introduces a card's contents, and the one that closes it. Both were
   inline `margin:0 0 16px` / `margin:14px 0 0` on settings.html, which is how a page
   ends up with four almost-equal gaps nothing aligns to. */
.note-lede  { margin: 0 0 var(--space-4); }
.note-after { margin: var(--space-3) 0 0; }

/* A value the tenant must act on — an unsaved warning, a missing wallet. The weight
   carries it as much as the colour, because colour alone is invisible to some readers
   and the whole point is that it is noticed. */
.text-bad { color: var(--dash-bad-ink); font-weight: 700; }

/* A card nested INSIDE another card — a payment method inside the methods card. It keeps
   the border and the padding and drops the shadow, because a shadow inside a shadow reads
   as a rendering fault rather than as depth. */
.card-nested { box-shadow: none; }

/* The smallest readable line: a field's own footnote, below its help text. Uses the ink
   that passes AA on the panel (the faint one does not at this size), which is the whole
   reason this is a class and not `font-size:12px` repeated. */
.note-fine {
  margin: 0 0 var(--space-3);
  font-size: var(--text-xs);
  line-height: var(--leading);
  color: var(--dash-ink-muted);
}

/* "(optional)" beside a label. Normal weight inside a bold label, so it reads as an
   aside rather than as part of the field's name. */
.label-aside { font-weight: 400; }

/* A switch row whose label wraps to two lines: align to the top so the track sits beside
   the FIRST line rather than floating in the middle of the block. */
.switch-top { align-items: flex-start; }

/* ================================================================== tiles */

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(212px, 1fr));
  gap: var(--space-4);
  margin-block-end: 18px;
}

.tile {
  position: relative;
  overflow: hidden;
  background: var(--dash-panel);
  border: 1px solid var(--dash-line);
  border-radius: var(--dash-radius);
  box-shadow: var(--dash-shadow);
  padding: var(--space-4) var(--space-5);
}
/* A 3px gradient cap, the control centre's KPI signature. */
.tile::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: 3px;
  background: var(--dash-grad);
}
.tile--gold::before  { background: var(--dash-grad-gold); }
.tile--green::before { background: var(--dash-tile-grad-green); }
.tile--red::before   { background: var(--dash-tile-grad-red); }

.tile .head { display: flex; align-items: center; gap: var(--space-2); margin-block-end: 10px; }
.tile .icon {
  inline-size: 30px; block-size: 30px; border-radius: var(--radius-sm);
  display: grid; place-items: center; flex: none;
  background: var(--dash-brand-soft); color: var(--dash-brand-ink);
}
.tile .icon svg { inline-size: 15px; block-size: 15px; }
.tile .label {
  font-size: var(--text-xs); font-weight: 800; letter-spacing: .05em;
  text-transform: uppercase; color: var(--dash-ink-faint);
}
.tile .value {
  font-size: var(--text-2xl); font-weight: 800; line-height: 1.1;
  letter-spacing: -.02em; font-variant-numeric: tabular-nums;
}

.tile--blue   .icon { background: var(--dash-brand-soft);  color: var(--dash-brand-ink); }
.tile--green  .icon { background: var(--dash-ok-soft);     color: var(--dash-ok-ink); }
.tile--purple .icon { background: var(--dash-accent-soft); color: var(--dash-accent-ink); }
.tile--red    .icon { background: var(--dash-bad-soft);    color: var(--dash-bad-ink); }
.tile--gold   .icon { background: var(--dash-gold-soft);   color: var(--dash-warn-ink); }

.delta {
  display: inline-flex; align-items: center; gap: var(--space-1);
  margin-block-start: 9px;
  padding: var(--space-px) var(--space-2);
  border-radius: var(--radius-pill);
  font-size: var(--text-xs); font-weight: 800;
}
.delta.up   { background: var(--dash-ok-soft);   color: var(--dash-ok-ink); }
.delta.down { background: var(--dash-bad-soft);  color: var(--dash-bad-ink); }
/* ink-faint on line-soft does not clear 4.5:1 either (D-274) — ink-muted,
   same reasoning as .chip--none just above. */
.delta.flat { background: var(--dash-line-soft); color: var(--dash-ink-muted); }
/* NOT `opacity`. Fading --dash-ink-muted to 85% on the flat pill's tinted background
   measured 3.80:1 against a 4.5 floor — and it passed the contrast gate for months,
   because the gate read `color` and never looked at opacity. A real colour instead, so
   what is measured is what is painted: the de-emphasis comes from the weight and the
   smaller size, which is where it should have come from. */
.delta .since { font-weight: 600; }

/* ================================================================= charts */

.chart { display: block; inline-size: 100%; block-size: auto; }
.chart .grid-line { stroke: var(--dash-line); stroke-width: 1; }
.chart .bar { fill: url(#barGradient); }
.chart .axis { fill: var(--dash-ink-faint); font-size: var(--text-xs); font-weight: 700; }
.chart .value-label { fill: var(--dash-ink-muted); font-size: var(--text-xs); font-weight: 800; }

/* ================================================================== chips */

.chip {
  display: inline-flex; align-items: center;
  /* Left at the literal 6px, not var(--space-2) (8px): the 2px D-379 added
     here was enough, inside an .acc-head's two-row CSS grid, in Arabic, at
     390px, to push test_accordion.py's test_a_closed_section_is_only_its_
     header over its 96px ceiling on the USDT section specifically -- a
     closed section quietly showing more of itself than its header. The gap
     is purely decorative (chip dot to chip text) and has no reason to sit
     on the spacing rhythm a card's own layout does; snapping it up was not
     worth the regression it bought. */
  gap: 6px;
  font-size: var(--text-xs); font-weight: 800;
  letter-spacing: .02em;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--dash-line-soft);
  color: var(--dash-ink-muted);
  text-decoration: none;
  white-space: nowrap;
}
.chip::before {
  content: "";
  inline-size: 6px; block-size: 6px; border-radius: 50%;
  background: currentColor; flex: none;
}
.chip--plain::before { display: none; }
.chip--active    { background: var(--dash-ok-soft);    color: var(--dash-ok-ink); }
.chip--completed { background: var(--dash-ok-soft);    color: var(--dash-ok-ink); }
.chip--pending   { background: var(--dash-warn-soft);  color: var(--dash-warn-ink); }
.chip--expired   { background: var(--dash-bad-soft);   color: var(--dash-bad-ink); }
/* `--dash-ink-faint` on `--dash-line-soft` is 4.2:1 at best (D-274) — a shade
   too close to call a pass, and the point of these two chips is to be READ as
   "nothing", not to be unreadable. `--dash-ink-muted` is the next step up. */
.chip--cancelled { background: var(--dash-line-soft);  color: var(--dash-ink-muted); }
.chip--none      { background: var(--dash-line-soft);  color: var(--dash-ink-muted); }
.chip--brand     { background: var(--dash-brand-soft); color: var(--dash-brand-ink); }
/* A certificate that was taken back (D-302/D-333). Its own name rather than a reuse of
   `chip--expired`, because the two are different events a tenant acts on differently:
   expired ran out, withdrawn was a decision somebody made and can undo. */
.chip--withdrawn { background: var(--dash-bad-soft);   color: var(--dash-bad-ink); }

/* A chip used as a filter link: it has to look pressable. */
a.chip { border: 1px solid transparent; transition: border-color var(--dash-speed); }
a.chip:hover { border-color: var(--dash-brand); color: var(--dash-brand-ink); }

/* ================================================================ avatars */

.avatar {
  inline-size: 32px; block-size: 32px; border-radius: var(--radius-sm);
  display: grid; place-items: center; flex: none;
  color: var(--dash-on-brand); font-weight: 800; font-size: var(--text-sm);
  background: var(--dash-grad);
  text-transform: uppercase;
}
/* WHITE ON THESE IS NOT AA, and that is allowed only because the initial is decoration:
   every avatar in this product carries `aria-hidden="true"` and sits beside the full name it is
   the first letter of, so no information is behind the letter. Measured against white at each
   gradient's light end: -1 is 3.39:1, -2 3.48:1, -4 1.92:1, -5 2.52:1.
   
   If an avatar ever becomes the ONLY thing identifying somebody — a list with no names, a
   compact row — these have to be darkened to 4.5:1 first, and the contrast gate will say so the
   moment the `aria-hidden` comes off. */
.avatar-1 { background: var(--dash-avatar-grad-1); }
.avatar-2 { background: var(--dash-avatar-grad-2); }
.avatar-3 { background: var(--dash-avatar-grad-3); color: var(--dash-on-gold); }
.avatar-4 { background: var(--dash-avatar-grad-4); }
.avatar-5 { background: var(--dash-avatar-grad-5); }
.avatar-sm { inline-size: 26px; block-size: 26px; font-size: var(--text-xs); border-radius: var(--radius-sm); }

.person { display: flex; align-items: center; gap: var(--space-3); min-inline-size: 0; }
.person .who { min-inline-size: 0; }
.person .name { font-weight: 700; white-space: nowrap; overflow: hidden;
                text-overflow: ellipsis; display: block; }
.person .handle { font-size: var(--text-xs); color: var(--dash-ink-faint); }

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

/* `position: relative` is not decoration here, it is what makes the clipping work.
   
   `.visually-hidden` is `position: absolute`, and an absolutely positioned element is laid out
   against its nearest POSITIONED ancestor — not against whatever happens to have
   `overflow: auto`. With no positioned ancestor, a screen-reader-only span inside a 665px-wide
   table was positioned against the document and extended its scroll area to 485px in a 390px
   viewport: the table itself was contained perfectly (body.scrollWidth stayed 390) while the
   PAGE could be panned 95px sideways by anyone who tried (D-306).
   
   It went unnoticed because the subscribers table had no rows in any fixture until a support
   thread seeded a subscriber — a page measured in its empty state is a page whose real content
   nothing has looked at, which is the same lesson D-299 recorded about locked features. */
.table-wrap { overflow-x: auto; position: relative; }

table.data {
  inline-size: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: var(--text-base);
}
table.data th {
  text-align: start;
  font-size: var(--text-xs);
  font-weight: 800;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--dash-ink-faint);
  padding: var(--space-2) var(--space-4);
  border-block-end: 1px solid var(--dash-line);
  white-space: nowrap;
  background: var(--dash-panel);
}
/* A sortable column's header, as a plain link styled to match the th it replaces (D-351).
   Not `color: inherit` alone — a link needs its own `:hover`/`:focus-visible` to read as
   clickable, which the th's static text never had to provide. */
/* A table HEADER CELL is not 44px tall, so the link filling it is not either, unless it is
   given its own tap area explicitly (D-351/D-352). `table.data th` has
   `padding: var(--space-2) var(--space-4)` — roughly 8px vertical — which a text link
   inherits and nothing more; the negative margin cancels the cell's own padding so the
   enlarged tap target does not also enlarge the header ROW. */
.th-sort {
  color: inherit; text-decoration: none; display: inline-flex; gap: var(--space-1); align-items: center;
  justify-content: center;
  min-block-size: 44px; min-inline-size: 44px; padding-block: var(--space-3);
  margin-block: calc(-1 * var(--space-3));
}
.th-sort:hover, .th-sort:focus-visible { color: var(--dash-ink); }
.th-sort.is-active { color: var(--dash-ink); }

table.data td {
  padding: var(--space-3) var(--space-4);
  border-block-end: 1px solid var(--dash-line-soft);
  vertical-align: middle;
}
table.data tbody tr { transition: background var(--dash-speed); }
table.data tbody tr:hover { background: var(--dash-panel-2); }
table.data tbody tr:last-child td { border-block-end: 0; }
.num { font-variant-numeric: tabular-nums; }
.money { font-variant-numeric: tabular-nums; font-weight: 800; }

/* ============================================================ empty states */

.empty-state {
  display: grid;
  place-items: center;
  gap: var(--space-2);
  padding: var(--space-10) var(--space-5) var(--space-12);
  text-align: center;
}
.empty-art { inline-size: 120px; block-size: auto; display: block; }
.empty-state .title { margin: 0; font-size: var(--text-md); font-weight: 800; }
.empty-state .body  { margin: 0; color: var(--dash-ink-muted); font-size: var(--text-sm);
                      max-inline-size: 38ch; }
.empty-state .btn { margin-block-start: 8px; }

/* The plain one-liner, for a filter that matched nothing — there is no
   illustration here because the section is not actually empty. */
.empty { padding: var(--space-8) var(--space-5); text-align: center; color: var(--dash-ink-faint);
         font-weight: 600; }

/* No skeletons. There was a `.skeleton` / `@keyframes shimmer` block here, in both
   stylesheets, styling a loading state this product does not have: every page renders
   server-side and the only `fetch` in the whole of web/static/js is the verification
   poll, which replaces no content. Nothing ever carried the class. Deleted rather than
   kept "for later" — see tests/test_nothing_is_built_and_never_called.py. */

/* ================================================================= toasts */

.toast-stack {
  position: fixed;
  inset-block-end: 22px;
  inset-inline-end: 22px;
  display: grid;
  gap: var(--space-2);
  z-index: 90;
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-inline-size: 240px;
  max-inline-size: 380px;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius);
  background: var(--dash-panel);
  border: 1px solid var(--dash-line);
  box-shadow: var(--dash-shadow-lg);
  font-weight: 600;
  font-size: var(--text-base);
  animation: toast-in .28s cubic-bezier(.22,1,.36,1);
}
.toast.is-leaving { animation: toast-out .22s ease forwards; }
.toast-ok  { border-inline-start: 3px solid var(--dash-ok); }
.toast-bad { border-inline-start: 3px solid var(--dash-bad); }
/* The neutral one. "Nothing to change" is neither a success nor a problem, and it was
   coming out green on both the banner and the toast (REQUEST_AUDIT O25). */
.toast-info { border-inline-start: 3px solid var(--dash-brand, var(--dash-accent)); }
@keyframes toast-in  { from { opacity: 0; transform: translateY(12px) scale(.97); } }
@keyframes toast-out { to   { opacity: 0; transform: translateY(8px); } }

/* ================================================================ notices */

.alert {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  border-radius: var(--dash-radius-sm);
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-base);
  font-weight: 600;
  margin-block-end: 16px;
  text-align: start;
}
.alert-ok    { background: var(--dash-ok-soft);    color: var(--dash-ok-ink); }
.alert-error { background: var(--dash-bad-soft);   color: var(--dash-bad-ink); }
.alert-warn  { background: var(--dash-warn-soft);  color: var(--dash-warn-ink); }
.alert-info  { background: var(--dash-brand-soft); color: var(--dash-brand-ink); }

/* WHAT YOU ARE OVER, after a downgrade (D-331). An `.alert` is a flex row by default,
   which would lay the heading, the sentence, the list and the button out side by side —
   this is a block of prose with a list in it, so it stacks. */
.over-limit { display: block; }
.over-limit strong { display: block; margin-block-end: 4px; }
.over-limit p { margin: 0 0 var(--space-2); }
.over-limit ul { margin: 0 0 var(--space-3); padding-inline-start: 1.2em; }
.over-limit li { margin-block-end: 2px; }

/* THE BOT IS DOWN (D-323). An ordinary error alert carrying a button, which `.alert`'s
   `align-items: flex-start` would otherwise strand at the top of a two-line sentence.
   Wraps on a phone so the button lands under the words rather than squeezing them to
   one character a line. */
.bot-down { align-items: center; flex-wrap: wrap; gap: var(--space-2) var(--space-3); }
.bot-down strong { flex: none; }
.bot-down span { flex: 1 1 14rem; min-inline-size: 0; }
.bot-down .btn { flex: none; margin-inline-start: auto; }

.note {
  margin: var(--space-3) var(--space-5) var(--space-4);
  font-size: var(--text-xs);
  line-height: 1.55;
  color: var(--dash-ink-muted);
  background: var(--dash-panel-2);
  border: 1px solid var(--dash-line-soft);
  border-radius: var(--dash-radius-sm);
  padding: var(--space-3) var(--space-3);
}

/* ---------------------------------------- the owner is looking at somebody */

/* Deliberately loud, deliberately at the very top of the page, deliberately
   not dismissible: the whole risk of an account switcher is forgetting whose
   account you are in. Gold rather than red because nothing is wrong. */
.acting-banner {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-3) var(--space-6);
  background: var(--dash-grad-gold);
  color: var(--dash-on-gold);
  font-weight: 700;
  font-size: var(--text-sm);
}
.acting-banner svg { inline-size: 17px; block-size: 17px; flex: none; }
.acting-banner .spacer { flex: 1; }
.acting-banner form { margin: 0; }
.acting-banner .btn {
  background: rgba(0,0,0,.14);
  border-color: rgba(0,0,0,.18);
  color: var(--dash-on-gold);
}

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

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--dash-line);
  border-radius: var(--dash-radius-sm);
  background: var(--dash-panel);
  color: var(--dash-ink);
  font: inherit;
  font-size: var(--text-base);
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition: transform var(--dash-speed), box-shadow var(--dash-speed),
              background var(--dash-speed), border-color var(--dash-speed);
}
.btn:hover  { transform: translateY(-1px); box-shadow: var(--dash-shadow); }
.btn:active { transform: translateY(0); }

/* A bare icon macro carries only a viewBox, so an inline SVG inside a button
   needs an explicit size or it renders at the browser's oversized intrinsic
   default — found while screenshotting: "add a channel" became a huge circle. */
.btn svg { inline-size: 15px; block-size: 15px; flex: none; }

.btn-primary { background: var(--dash-grad);      border-color: transparent; color: var(--dash-on-brand); }
.btn-ok      { background: var(--dash-ok);  border-color: transparent; color: var(--dash-on-brand); }
.btn-bad     { background: var(--dash-bad); border-color: transparent; color: var(--dash-on-brand); }
.btn-outline { background: var(--dash-panel); }
.btn-ghost   { background: transparent; border-color: transparent;
               color: var(--dash-ink-muted); }
.btn-ghost:hover { color: var(--dash-brand-ink); background: var(--dash-brand-soft); }
.btn-sm      { padding: var(--space-1) var(--space-3); font-size: var(--text-sm); border-radius: var(--radius-sm); }
.btn-icon    { padding: var(--space-2); border-radius: var(--radius-sm); }
.btn-block   { inline-size: 100%; }

.btn:focus-visible,
.side-link:focus-visible,
.dash-who:focus-visible,
.input:focus-visible,
select.unit:focus-visible {
  outline: 2px solid var(--dash-brand);
  outline-offset: 2px;
}

.btn-row { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; }
/* The status gates (locked out, pending, suspended) have nothing beside the
   row to justify it to one side, so it reads better centred under the card
   copy than pinned to the inline-start edge every other `.btn-row` uses. */
.btn-row--center { justify-content: center; }

/* The dashboard table pager (D-351/D-352, generalised for every table D-353): centred
   like `.btn-row--center`, with its own top margin so it reads as its own row under
   the table rather than crowding it. Shared by `dashboard/_pager.html`'s macro, not
   one table's own class. */
.pager { margin-block-start: var(--space-4); }

/* A pager link at either end of the range. `visibility: hidden` rather than removing the
   element: the row stays the same width whichever end is reached, so the "next" link
   does not jump sideways when "previous" disappears on page one. */
.pager-edge { visibility: hidden; }

/* The "Page X of Y" text between the pager's two links — the dashboard's own muted-text
   colour, inline rather than as its own paragraph, so `.lede`'s block margins do not
   apply here. */
.pager-label { color: var(--dash-ink-muted); font-size: var(--text-sm); }

/* ================================================================= fields */

.input, select.unit, textarea.input {
  inline-size: 100%;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--dash-line);
  border-radius: var(--dash-radius-sm);
  background: var(--dash-field);
  color: var(--dash-ink);
  font: inherit;
  font-size: var(--text-base);
}
.input:focus, select.unit:focus {
  border-color: var(--dash-brand);
  box-shadow: 0 0 0 3px var(--dash-brand-soft);
  outline: none;
}
.input-sm { padding: var(--space-2) var(--space-3); font-size: var(--text-sm); }
select.unit { inline-size: 7rem; flex: none; }

.form-field { display: block; margin-block-end: 13px; }
.form-label {
  display: block;
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--dash-ink-muted);
  margin-block-end: 5px;
}
.form-help {
  display: block;
  margin-block-start: 4px;
  font-size: var(--text-xs);
  color: var(--dash-ink-faint);
  line-height: 1.5;
}
.field-row { display: flex; gap: var(--space-2); align-items: stretch; }
.field-row .input { flex: 1; min-inline-size: 0; }
/* The plans table edits name and description as two stacked one-field forms
   in the same cell — a sibling gap rather than an inline style on the second
   form, so the spacing is the same wherever two field-rows stack. */
.field-row + .field-row { margin-block-start: var(--space-1); }

/* Fixed widths for the inline edit fields on the plans table: just enough to
   show a short value without the row pushing the unlock pills and actions
   columns past a 1440px viewport (every other column is as wide as its
   content needs). */
.plan-field-name  { inline-size: 9.5rem; font-weight: 700; }
.plan-field-desc  { inline-size: 9.5rem; }
.plan-field-price { inline-size: 5.5rem; }
.plan-field-num   { inline-size: 4.5rem; }

/* A select that filters a list rather than carrying a value of its own —
   narrower than the full-width `.input` default because there is nothing
   beside it that needs the room. */
.select-narrow { inline-size: 9rem; flex: none; }

.price-with-currency { display: flex; align-items: center; gap: var(--space-2); }
.price-with-currency .input { flex: 1; min-inline-size: 0; }
.currency-badge {
  flex: none;
  font-size: var(--text-xs);
  font-weight: 800;
  color: var(--dash-ink-muted);
  background: var(--dash-line-soft);
  border-radius: var(--radius-pill);
  padding: var(--space-1) var(--space-3);
}

.search {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--dash-line);
  border-radius: var(--radius-pill);
  background: var(--dash-field);
}
.search input {
  border: 0; background: none; font: inherit; font-size: var(--text-sm);
  color: inherit; inline-size: 13rem; min-inline-size: 0;
}
/* The input is borderless inside the pill, so the ring belongs to the WRAPPER:
   on the input it would be drawn inside the pill's own edge and read as a
   rendering fault. `outline: none` here without this rule left the box with no
   painted focus state whatsoever -- measured as zero changed pixels between
   blurred and keyboard-focused -- on Subscribers, Payments and Switch, which is
   a keyboard user unable to tell where they are (WCAG 2.4.7). */
.search input:focus,
.search input:focus-visible { outline: none; }
.search:focus-within {
  border-color: var(--dash-brand);
  box-shadow: var(--ring);
}
.search svg { inline-size: 15px; block-size: 15px; color: var(--dash-ink-faint); flex: none; }

/* ---------------------------------------------------------------- switches */

.switch { position: relative; display: inline-flex; align-items: center; gap: var(--space-2);
          cursor: pointer; }
.switch input { position: absolute; opacity: 0; inline-size: 1px; block-size: 1px; }
.switch .track {
  inline-size: 40px; block-size: 22px; border-radius: var(--radius-pill);
  background: var(--dash-line); position: relative; flex: none;
  transition: background var(--dash-speed);
}
.switch .track::after {
  content: "";
  position: absolute;
  inset-block-start: 2px;
  inset-inline-start: 2px;
  inline-size: 18px; block-size: 18px;
  border-radius: 50%;
  background: var(--dash-white);
  box-shadow: 0 1px 3px rgba(0,0,0,.25);
  transition: inset-inline-start var(--dash-speed);
}
.switch input:checked + .track { background: var(--dash-grad); }
.switch input:checked + .track::after { inset-inline-start: 20px; }
.switch input:focus-visible + .track { outline: 2px solid var(--dash-brand);
                                       outline-offset: 2px; }

/* ================================================== popovers and confirms */

/* The native Popover API needs no script at all: [popovertarget] opens the
   element carrying a bare `popover` attribute and
   [popovertargetaction="hide"] closes it. That is what keeps every confirm
   dialog and every menu on this surface working under a CSP with no
   'unsafe-inline' on script-src. The previous version of the plans page used
   an onchange attribute instead, which silently did nothing in a real browser. */
[popover] {
  border: 1px solid var(--dash-line);
  border-radius: var(--dash-radius);
  box-shadow: var(--dash-shadow-lg);
  padding: var(--space-4);
  max-inline-size: min(92vw, 360px);
  background: var(--dash-panel);
  color: var(--dash-ink);
  font: inherit;
}
[popover]::backdrop { background: rgba(10,16,40,.28); }
.confirm-pop p { margin: 0 0 var(--space-4); font-size: var(--text-base); line-height: 1.55; }
.confirm-pop .row { display: flex; gap: var(--space-2); justify-content: flex-end;
                    flex-wrap: wrap; }
.confirm-pop form { margin: 0; }

/* ============================================================== checklist */

.checklist {
  margin-block-end: 18px;
  background: var(--dash-panel);
  border: 1px solid var(--dash-line);
  border-radius: var(--dash-radius);
  box-shadow: var(--dash-shadow);
  padding: var(--space-5) var(--space-5);
  position: relative;
  overflow: hidden;
}
.checklist::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: 3px;
  background: var(--dash-grad-gold);
}
.checklist h2 { margin: 0 0 var(--space-1); font-size: var(--text-md); font-weight: 800; }
.checklist .lede { margin: 0 0 var(--space-4); font-size: var(--text-sm); color: var(--dash-ink-muted); }
.checklist ol {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--space-3);
}
.checklist li {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-3);
  border: 1px solid var(--dash-line);
  border-radius: var(--dash-radius-sm);
  background: var(--dash-panel-2);
  font-size: var(--text-sm);
  font-weight: 700;
}
.checklist li.done {
  background: var(--dash-ok-soft);
  border-color: transparent;
  color: var(--dash-ok-ink);
}
.checklist .step-mark {
  inline-size: 26px; block-size: 26px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  background: var(--dash-panel);
  border: 2px solid var(--dash-line);
  color: var(--dash-ink-faint);
  font-size: var(--text-xs); font-weight: 800;
}
.checklist li.done .step-mark {
  background: var(--dash-ok); border-color: var(--dash-ok); color: var(--dash-on-brand);
}
.checklist .step-mark svg { inline-size: 13px; block-size: 13px; }
.checklist .step-text { flex: 1; min-inline-size: 0; }
.checklist .step-cta {
  margin-inline-start: auto;
  font-size: var(--text-xs);
  font-weight: 800;
  color: var(--dash-brand-ink);
  text-decoration: none;
  white-space: nowrap;
}
.checklist .step-cta:hover { text-decoration: underline; }

/* ========================================================== destinations */

.dest-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-3) 0;
  border-block-end: 1px solid var(--dash-line-soft);
}
.dest-row:last-child { border-block-end: 0; }
.dest-row .kind {
  inline-size: 34px; block-size: 34px; border-radius: var(--radius-sm); flex: none;
  display: grid; place-items: center;
  background: var(--dash-brand-soft); color: var(--dash-brand-ink);
}
.dest-row .kind svg { inline-size: 17px; block-size: 17px; }
.dest-row .meta { flex: 1; min-inline-size: 0; }
.dest-row .meta strong { display: block; font-size: var(--text-base); }
.dest-row .meta small { color: var(--dash-ink-faint); font-size: var(--text-xs); }
.dest-row.is-off .kind { background: var(--dash-line-soft); color: var(--dash-ink-faint); }
.dest-row.is-off .meta strong { color: var(--dash-ink-muted); }

/* The per-plan unlock toggles. Each is its own one-button form, so there is no
   change handler to attach and nothing silently fails with JS off. */
.unlock-row { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.unlock-row form { margin: 0; }
.unlock {
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-xs);
  border-radius: var(--radius-pill);
}

/* F1, build item 3: the caption for a plan with no pill pressed in above — not
   "unlocks nothing", `destinations_for_plan`'s own implicit rule. Muted the same
   way every other secondary line on this dashboard is, never the warning tint
   `.dir-warn` wears — nothing here is a problem to fix, just a fact worth saying. */
.plan-implicit-note {
  margin-block-start: 6px;
  font-size: var(--text-xs);
  color: var(--dash-ink-muted);
}

/* ================================================= login and status gates */

.gate {
  min-block-size: 100vh;
  display: grid;
  place-items: center;
  padding: var(--space-6);
  background:
    radial-gradient(900px 480px at 80% -10%, var(--dash-brand-soft), transparent 70%),
    radial-gradient(700px 420px at 10% 110%, var(--dash-accent-soft), transparent 70%),
    var(--dash-bg);
}
.gate-card {
  background: var(--dash-panel);
  border: 1px solid var(--dash-line);
  border-radius: var(--radius-lg);
  box-shadow: var(--dash-shadow-lg);
  padding: var(--space-10) var(--space-8);
  inline-size: min(100%, 460px);
  text-align: center;
}
.gate-card .brand { justify-content: center; margin-block-end: 20px; }
.gate-card h1 { margin: 0 0 var(--space-2); font-size: var(--text-xl); font-weight: 800; }
.gate-card p  { margin: 0 0 var(--space-5); color: var(--dash-ink-muted); }
.gate-widget { display: flex; justify-content: center; min-block-size: 46px; }
.gate-foot { margin-block-start: 20px; font-size: var(--text-sm); color: var(--dash-ink-faint); }
.gate-card .seg { margin-block-start: 18px; }

/* ============================================================ guide notice */

/* Restated here rather than pulled in with tgaccess.css — see the file header
   for why the dashboard no longer links the site's stylesheet. */
.guide-bar {
  background: var(--dash-grad);
  color: var(--dash-on-brand);
  font-size: var(--text-base);
}
.guide-bar[hidden] { display: none; }
.guide-bar-inner {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-2) var(--space-6);
}
.guide-bar-icon { font-size: var(--text-md); flex: none; }
.guide-bar p { margin: 0; flex: 1; min-inline-size: 0; font-weight: 600; }
/* A DARK glass pill, not a light one. White text on a 17%-white pill over the brand
   gradient measures 4.17:1 at the blue end and 4.53:1 at the purple end — so the very
   same button passed AA in English and failed it in Arabic, because RTL moves it to the
   other end of the gradient. Tinting the pill down instead of up puts it at ~7.5:1
   everywhere, and still reads as a pill on a coloured bar. */
.guide-bar-cta {
  flex: none;
  background: rgba(0,0,0,.18);
  color: var(--dash-on-brand); text-decoration: none;
  padding: var(--space-1) var(--space-4); border-radius: var(--radius-pill);
  font-weight: 700; font-size: var(--text-sm);
  border: 1px solid rgba(255,255,255,.3);
  white-space: nowrap;
}
.guide-bar-cta:hover { background: rgba(0,0,0,.28); }
.guide-bar-x {
  flex: none;
  /* Full white, not 80%. On the brand gradient `rgba(255,255,255,.8)` is 4.34:1, and a
     dismiss cross nobody can see is a bar nobody can close. The hover state below used to
     be the only place it reached full white. */
  background: none; border: 0; color: var(--dash-on-brand);
  font-size: var(--text-xl); line-height: 1; cursor: pointer; padding: 0 var(--space-1);
}
.guide-bar-x:hover { color: var(--dash-on-brand); }

/* The floating guide button is redundant on the dashboard — the guide already
   has a permanent home in the sidebar — and it covers the bottom-end corner
   where toasts appear. Hidden rather than removed from the include, which is
   shared with the public site. */
body.dash .guide-fab { display: none; }

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

@media (max-width: 1080px) {
  .dash-grid-2 { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 900px) {
  .dash-shell { grid-template-columns: minmax(0, 1fr); }
  .dash-side {
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    inline-size: 258px;
    z-index: 95;
    transform: translateX(-100%);
    transition: transform .26s cubic-bezier(.22,1,.36,1);
  }
  [dir="rtl"] .dash-side { transform: translateX(100%); }
  .dash-side.is-open { transform: none; }
  .dash-body { padding: var(--space-4) var(--space-4) var(--space-16); }
  .dash-top  { padding: var(--space-3) var(--space-4); }
  .guide-bar-inner { padding-inline: var(--space-4); }
  .acting-banner { padding-inline: var(--space-4); }
  .side-toggle { display: inline-flex !important; }
  .toast-stack { inset-inline: 14px; inset-block-end: 14px; }
  .toast { max-inline-size: none; }

  /* A 44px floor for every control, because a thumb is not a mouse pointer. Measured on
     a 390px viewport, the menu button and the theme button were 33x33, the language pills
     40x29 and 32x29, the guide-bar cross 26x22, the view-site link 41x25, a plans row's
     save button 41x40 and the USDT network switches 40x22 — and the menu button is the
     ONLY way to reach navigation at this width, so missing it is not cosmetic. 44px
     rather than Lighthouse's 48 because it is Apple's documented floor and it is what
     these controls come to once their own padding is allowed to do the work.
     .dash-top already wraps, so nothing overflows. */
  /* EVERY control, both dimensions. The first version of this rule listed only the icon
     buttons and the small ones, because the gate that found them compared `height < 44 &&
     width < 44` — which fires only when a control is small in BOTH, and no text link ever
     is. With the `||` Lighthouse actually uses, a 40px-tall primary button, a 29px chip
     link and a 19px text CTA all fail, and all three are things a thumb has to hit. */
  .btn, .btn-icon, .btn-sm,
  .chip, .seg a, .seg button,
  .guide-bar-x, .guide-bar-cta,
  .dash-who, .side-link,
  .step-cta, a.brand, .note a,
  .switch {
    min-inline-size: 44px;
    min-block-size: 44px;
  }
  /* The switch keeps its 40x22 track — only the label's hit area grows around it, so a row
     of network toggles looks the same and stops being a 22px-tall target. Same for the two
     that are naturally wide: a minimum width would only add dead space. */
  .side-link, .step-cta, a.brand, .note a { min-inline-size: 0; }
  /* The switch keeps its 44px minimum in BOTH directions: its track is 40px wide, so
     leaving the width unconstrained left a 40x44 target — tall enough for a thumb and
     narrow enough to miss. The track itself is unchanged; only the label's hit area grows
     around it, so a row of network toggles looks exactly as it did. */
  .switch { min-inline-size: 44px; }
  /* Anything whose content no longer fills its box centres it, rather than sitting at the
     top of 44px of nothing. */
  .guide-bar-x, .guide-bar-cta, .seg a, .seg button, .chip, .step-cta, a.brand,
  .note a {
    /* inline-flex, not just a minimum: `min-block-size` does nothing to a non-replaced
       INLINE element, which is why the brand link stayed 14px tall while the rule matched
       it perfectly. */
    display: inline-flex; align-items: center; justify-content: center;
  }
}

@media (min-width: 901px) { .side-toggle { display: none !important; } }

@media (max-width: 560px) {
  .dash-top h1 { font-size: var(--text-lg); }
  .dash-top { gap: var(--space-2); }
  .tiles { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
  .tile .value { font-size: var(--text-xl); }
  .dash-grid-cards, .dash-grid-even { grid-template-columns: minmax(0, 1fr); }
  .checklist ol { grid-template-columns: minmax(0, 1fr); }
  .dash-who .name { display: none; }
  .search input { inline-size: 8rem; }
  .card > header { padding: var(--space-3) var(--space-4); }
  table.data th, table.data td { padding: var(--space-2) var(--space-3); }
  .note { margin-inline: var(--space-4); }
  /* The label is the only thing that has to go; the icon still says which. */
  .viewsite-label { display: none; }
  /* A `.chip--plain` used as a header subtitle carries a full sentence on
     plans.html and invoices.html (and this is a plain phrase everywhere
     else, where the cap never bites) -- `white-space: nowrap` with no limit
     on its own width overflowed the viewport at 390px, because nothing
     here ever asked a chip to be narrower than its content. Truncated with
     an ellipsis rather than left to wrap: the pill shape's 999px radius on
     a two-line box reads as broken, not as a bigger pill. */
  .chip { max-inline-size: 65vw; overflow: hidden; text-overflow: ellipsis; }
}

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

/* Everything above that moves is decoration. Somebody who asked for less
   motion keeps the colour and the layout and loses the movement. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .btn:hover, .side-link:hover svg { transform: none; }
}

/* Every icon has a size of its own.
   An inline <svg> that declares only a viewBox has no intrinsic size, so CSS
   gives it the replaced-element default of 300x150px. That is how one icon in
   an alert box rendered as a full-width illustration. The context-specific
   rules above still win on specificity; this is the floor beneath them, so an
   icon dropped into new markup is never unconstrained. */
.dicon {
  inline-size: 1.15em;
  block-size: 1.15em;
  flex: none;
  vertical-align: -.16em;
}

/* The sign-in shield's wrapper. A bare <span> is inline, so it takes its height
   from line-height (14px) while the icon inside is 26px — the icon overflowed
   its own container by twelve pixels.

   Written here rather than in admin.css because /admin/login loads
   dashboard.css and not admin.css. The first version of this fix went into the
   file that page never loads, and the measurement caught that too. */
.gate-card > .mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* ============================================================================
   Paying a commission invoice
   ============================================================================
   USDT only, inside the invoice it settles. The amount comes first because it
   is the thing that must be exact and the thing most likely to be rounded; the
   network second, because choosing wrong loses the money; the hash last,
   because there is nothing to paste until the other two are done.
   ========================================================================= */

.pay-panel .lede { margin: var(--space-px) 0 0; font-size: var(--text-base); color: var(--dash-ink-muted); }

.pay-label {
  display: block;
  font-size: var(--text-xs);
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--dash-ink-faint);
  margin-block-end: 4px;
}

.pay-amount {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-4);
  border-radius: var(--radius);
  background: var(--dash-brand-soft);
  margin-block-end: 6px;
}
.pay-amount .pay-label { flex: 1 0 100%; margin: 0 0 var(--space-px); }
.pay-figure {
  font-size: var(--text-2xl);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
  color: var(--dash-brand-ink);
}
.pay-unit { font-weight: 800; color: var(--dash-ink-muted); }
.pay-why { font-size: var(--text-xs); color: var(--dash-ink-muted);
           margin: 0 0 var(--space-4); line-height: 1.55; }

/* ---- network picker ------------------------------------------------------
   Radios, not tabs-by-script: exactly one address is on screen at a time and it
   works with no JavaScript at all. Showing four addresses together is how a
   tenant sends TRC20 money to an ERC20 address. */

.pay-nets { border: 0; padding: 0; margin: 0 0 16px; }
.pay-nets legend { font-size: var(--text-sm); font-weight: 700; margin-block-end: 8px; }

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

.pay-net {
  display: inline-block;
  padding: var(--space-2) var(--space-4);
  margin-inline-end: var(--space-2);
  margin-block-end: 8px;
  border-radius: var(--radius-pill);
  border: 1.5px solid var(--dash-line);
  font-size: var(--text-sm);
  font-weight: 700;
  cursor: pointer;
  transition: border-color .18s, background .18s, color .18s;
}
.pay-net:hover { border-color: var(--dash-brand); }
.pay-radio:checked + .pay-net {
  border-color: var(--dash-brand);
  background: var(--dash-brand);
  color: var(--dash-on-brand);
}
.pay-radio:focus-visible + .pay-net {
  outline: 2px solid var(--dash-brand); outline-offset: 2px;
}

/* Only the chosen network's details. `display:none` rather than visibility or
   opacity, so a hidden address is not selectable, not searchable on the page,
   and not read out by a screen reader — three ways somebody could otherwise
   take the wrong one. */
.pay-detail { display: none; }
.pay-nets .pay-radio:nth-of-type(1):checked ~ .pay-detail:nth-of-type(1),
.pay-nets .pay-radio:nth-of-type(2):checked ~ .pay-detail:nth-of-type(2),
.pay-nets .pay-radio:nth-of-type(3):checked ~ .pay-detail:nth-of-type(3),
.pay-nets .pay-radio:nth-of-type(4):checked ~ .pay-detail:nth-of-type(4) {
  display: block;
}

.pay-warn { font-size: var(--text-sm); margin-block-end: 12px; }

.pay-address {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-block-end: 14px;
}
.pay-address code {
  flex: 1 1 240px;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--dash-panel-2);
  border: 1px solid var(--dash-line);
  font-size: var(--text-sm);
  word-break: break-all;
}

.pay-qr { margin-block-end: 6px; }
.pay-qr svg {
  inline-size: 132px;
  block-size: 132px;
  padding: var(--space-2);
  background: var(--dash-white);
  border: 1px solid var(--dash-line);
  border-radius: var(--radius-sm);
}
.pay-qr .qrline { stroke: var(--dash-qr-ink); }

/* The dashboard home's bot-link card: the QR beside its text and address, reusing
   .pay-qr's own sizing (D-359 piece 2) rather than a second definition for the same
   box. Stacks at phone widths like every other side-by-side card on this page. */
.bot-qr { display: flex; gap: var(--space-4); align-items: flex-start; flex-wrap: wrap; }
.bot-qr .pay-qr { margin-block-end: 0; }
.bot-qr-body { flex: 1 1 200px; min-inline-size: 0; }

.pay-declare { margin-block-start: 6px; }
.pay-declare label { display: block; font-weight: 700; font-size: var(--text-sm);
                     margin-block-end: 6px; }

@media (max-width: 600px) {
  .pay-figure { font-size: var(--text-xl); }
  .pay-address code { flex-basis: 100%; }
}

.btn.is-copied { background: var(--dash-ok); border-color: var(--dash-ok); color: var(--dash-on-brand); }

/* ============================================================ plan boundary */

/* The upsell. Centred and quiet: a tenant who has just been stopped is not in
   the mood for a sales page, and the useful information is two sentences —
   which feature, and which plan has it. */
.locked-card {
  display: grid;
  justify-items: center;
  gap: var(--space-3);
  text-align: center;
  padding: var(--space-10) var(--space-6);
}
.locked-card h2 { margin: 0; font-size: var(--text-lg); }
.locked-card .locked-what {
  margin: 0; color: var(--dash-ink-muted); max-inline-size: 44ch;
}
.locked-card .locked-lede { margin: 0; font-weight: 600; }
.locked-note {
  margin: var(--space-2) 0 0;
  font-size: var(--text-sm);
  color: var(--dash-ink-muted);
  max-inline-size: 46ch;
}

.locked-ico {
  display: grid; place-items: center;
  inline-size: 52px; block-size: 52px;
  border-radius: 50%;
  background: var(--dash-panel-2);
  color: var(--dash-ink-muted);
}
/* The icon macro takes no class argument, so the wrapper sizes it. */
.locked-ico .dicon { inline-size: 26px; block-size: 26px; }

/* The inline form, used where one section of a page is locked rather than the
   page. A row, not a centred block: it sits between two live sections and has
   to read as a gap in the page, not as the end of it. */
.locked-inline {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-5) var(--space-5);
  flex-wrap: wrap;
}
.locked-inline .locked-ico { inline-size: 40px; block-size: 40px; flex: none; }
.locked-inline .locked-ico .dicon { inline-size: 20px; block-size: 20px; }
.locked-inline h2 { margin: 0; font-size: var(--text-md); }
.locked-inline p  { margin: var(--space-px) 0 0; font-size: var(--text-sm); color: var(--dash-ink-muted); }

/* ------------------------------------------- connect your own bot (item 5b) */

/* A card with an action, where the switcher used to print a fact and leave the
   owner to work out what to do about it. A row rather than a centred block: it
   sits above the list of everybody else's tenancies and has to read as the
   first entry in that list, not as an interruption before it. */
.connect-own {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  padding: var(--space-5) var(--space-5);
}
.connect-own-ico {
  display: grid; place-items: center;
  inline-size: 42px; block-size: 42px;
  flex: none;
  border-radius: var(--radius-sm);
  background: var(--dash-brand);
  color: var(--dash-on-brand);
}
.connect-own-ico .dicon { inline-size: 22px; block-size: 22px; }
.connect-own-copy h2 { margin: 0; font-size: var(--text-md); }
.connect-own-copy p {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
  color: var(--dash-ink-muted);
  max-inline-size: 52ch;
}

/* The owner's own row is sorted first and badged, on the NAME's line. `.name` is
   a block with an ellipsis, so a badge after it would start a third line and make
   every row in the table taller. */
.person .name-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-inline-size: 0;
}
.person .name-row .name { min-inline-size: 0; }

/* Tighter than a normal chip so it fits inside the name's line box. At the
   standard 3px 10px it stood 3px taller than the line and made the owner's row
   taller than every other row in the table — a badge on one row should not
   change the table's rhythm. */
.chip--mine {
  flex: none;
  padding: var(--space-px) var(--space-2);
  font-size: var(--text-xs);
  line-height: 1.5;
}

/* --------------------------------------------------- niche packs (item 6) */

/* One row per pack: a switch, a name, what it is for, and the features it
   carries. The feature chips are the point of the row — a tenant deciding
   whether to switch a pack on is deciding about those, not about the name — so
   they are listed rather than counted. */
/* A CARD per pack (D-272), not a list row. Each carries a name, a sentence, a
   switch and the features it brings; at list-row width those feature lists wrapped
   into a paragraph nobody read.

   `auto-fit` with a 17rem floor rather than a fixed column count: four packs today
   and the owner can define more from /admin, so the grid has to decide for itself
   how many fit. */
.pack-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: var(--space-3);
  margin-block-end: var(--space-5);
}
.pack-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  border: 1px solid var(--dash-line);
  border-radius: var(--dash-radius);
  background: var(--dash-panel);
  transition: border-color var(--motion), background var(--motion),
              box-shadow var(--motion);
}
/* On is a border and a tint, never colour alone: the switch says it too, and a
   tenant who cannot distinguish the tint still has the switch and the border. */
.pack-card.is-on {
  border-color: var(--dash-brand);
  background: var(--dash-brand-soft);
  box-shadow: inset 0 0 0 1px var(--dash-brand);
}
.pack-card-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.pack-card-head h3 {
  margin: 0;
  flex: 1;
  min-inline-size: 0;
  font-size: var(--text-md);
  line-height: var(--leading-snug);
}
/* The switch keeps its own size from `.switch .track`; this only stops the flex
   row from squeezing it when the name is long. */
.pack-card-head .switch { flex: none; }
.pack-card-body {
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--leading);
  color: var(--dash-ink-muted);
}
.pack-features {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
  margin: auto 0 0;          /* pinned to the bottom, so cards align in a row */
  padding-block-start: var(--space-2);
}
.pack-label {
  font-size: var(--text-xs);
  font-weight: 800;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--dash-ink-faint);
}
/* A feature the plan does not reach, named rather than hidden (D-226). The lock
   makes it legible without the colour, for the same reason as above. */
.pack-features .chip--pending {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}
.pack-features .chip--pending .dicon {
  inline-size: 11px;
  block-size: 11px;
}

/* ------------------------------------------------ the setup wizard (P3) */

/* The progress bar. A bar with no number is a bar you cannot read out, so the
   count sits beside it and says the same thing. */
.setup-progress { padding: var(--space-5) var(--space-5); }
.setup-bar {
  block-size: 8px;
  border-radius: var(--radius-pill);
  background: var(--dash-line-soft);
  overflow: hidden;
}
.setup-bar-fill {
  display: block;
  block-size: 100%;
  background: var(--dash-brand);
  border-radius: var(--radius-pill);
  transition: inline-size .3s ease;
}
.setup-count {
  margin: var(--space-3) 0 0;
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--dash-ink-muted);
  font-variant-numeric: tabular-nums;
}

.setup-steps { list-style: none; margin: var(--space-5) 0 0; padding: 0; display: grid; gap: var(--space-3); }
.setup-step {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--dash-line);
  border-radius: var(--radius-sm);
  background: var(--dash-panel);
}
/* The step they are on, and the ones behind them. Done is quieter, not hidden —
   a finished step is where that thing is edited from. */
.setup-step.is-current { border-color: var(--dash-brand); background: var(--dash-brand-soft); }
.setup-step.is-done .setup-copy h2 { color: var(--dash-ink-muted); }

.setup-mark {
  display: grid; place-items: center;
  inline-size: 30px; block-size: 30px;
  flex: none;
  border-radius: 50%;
  border: 1.5px solid var(--dash-line);
  font-size: var(--text-sm); font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--dash-ink-muted);
}
.setup-step.is-done .setup-mark {
  background: var(--dash-ok);
  border-color: transparent;
  color: var(--dash-on-brand);
}
.setup-step.is-done .setup-mark .dicon { inline-size: 16px; block-size: 16px; }
.setup-step.is-current .setup-mark { border-color: var(--dash-brand); color: var(--dash-brand-ink); }

.setup-copy { min-inline-size: 0; }
.setup-copy h2 {
  margin: 0; font-size: var(--text-md);
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
}
.setup-copy p {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
  color: var(--dash-ink-muted);
  max-inline-size: 56ch;
}
.setup-step form { display: flex; gap: var(--space-2); flex-wrap: wrap; }

/* The finished state: a summary with a badge, because the steps below it are still
   where each thing is edited. */
.setup-done { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; padding: var(--space-5); }
.setup-done h2 { margin: 0; font-size: var(--text-md); }
.setup-done p { margin: var(--space-1) 0 0; font-size: var(--text-sm); color: var(--dash-ink-muted); }
.setup-badge {
  display: grid; place-items: center;
  inline-size: 40px; block-size: 40px;
  flex: none;
  border-radius: 50%;
  background: var(--dash-ok);
  color: var(--dash-on-brand);
}
.setup-badge .dicon { inline-size: 22px; block-size: 22px; }

/* The nav link, while setup is unfinished. */
.side-link.is-setup .side-badge { background: var(--dash-brand); color: var(--dash-on-brand); }

/* The nav link while the BOT IS DOWN (D-323). Tinted rather than merely coloured: the
   sidebar's ink is already light on a dark ground, so red text alone reads as a slightly
   different grey — and colour on its own is never the only signal here (D-297), which is
   what the dot is for. */
.side-link.is-urgent {
  background: rgba(228, 74, 74, .16);
  color: var(--dash-side-urgent-ink);
}
.side-link.is-urgent::after {
  content: "";
  inline-size: 7px; block-size: 7px; border-radius: 50%;
  background: var(--dash-side-urgent-dot); margin-inline-start: auto; flex: none;
}

/* ------------------------------------------ the owner switcher (item 7) */

/* The summary row, which IS the filter. Numbers big enough to read across the
   page, labels small enough not to compete with them. */
.switch-summary {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-block-end: 16px;
}
.switch-stat {
  display: grid;
  gap: var(--space-px);
  padding: var(--space-3) var(--space-5);
  min-inline-size: 84px;
  border: 1px solid var(--dash-line);
  border-radius: var(--radius-sm);
  background: var(--dash-panel);
  text-decoration: none;
  color: inherit;
  transition: border-color var(--dash-speed), background var(--dash-speed);
}
.switch-stat:hover { border-color: var(--dash-brand); }
.switch-stat.is-on { border-color: var(--dash-brand); background: var(--dash-brand-soft); }
/* A count of zero is still a filter — clicking it proves the answer is none rather
   than leaving you wondering — but it does not ask for attention. */
.switch-stat.is-zero .switch-stat-n { color: var(--dash-ink-faint); }
.switch-stat-n {
  font-size: var(--text-xl); font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.switch-stat-label {
  font-size: var(--text-xs); font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--dash-ink-muted);
}

/* What a pending tenant is still missing, under their status. A list rather than a
   sentence: the owner is scanning for what to chase, not reading. */
.switch-missing {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-1);
  font-size: var(--text-xs);
  color: var(--dash-ink-muted);
}
.switch-missing li::before {
  content: "⬜️";
  margin-inline-end: var(--space-1);
  font-size: var(--text-xs);
}
.switch-remind { margin-block-end: 6px; }

/* --------------------------------------- the people directory (item 8) */

/* The actions cell. Extend is a row of small buttons because each is one tap;
   ending access is behind a <details> because it takes something away and should
   need a deliberate open. */
.dir-actions { min-inline-size: 12rem; }
.dir-extend {
  display: flex; align-items: center; gap: var(--space-1); flex-wrap: wrap;
}
.dir-extend-label {
  font-size: var(--text-xs); font-weight: 800;
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--dash-ink-faint);
  margin-inline-end: var(--space-px);
}
.dir-extend .btn { padding-inline: var(--space-2); font-variant-numeric: tabular-nums; }

.dir-revoke { margin-block-start: 6px; }
/* The marker is removed and the summary styled as a button, so the control reads as
   one thing rather than as a disclosure triangle beside a word. */
.dir-revoke > summary { list-style: none; cursor: pointer; }
.dir-revoke > summary::-webkit-details-marker { display: none; }
.dir-revoke[open] > summary { color: var(--dash-bad-ink); }
.dir-revoke form {
  display: grid; gap: var(--space-2);
  margin-block-start: 8px;
  padding: var(--space-3);
  border: 1px solid var(--dash-bad);
  border-radius: var(--radius-sm);
  background: var(--dash-bad-soft);
  min-inline-size: 15rem;
}
.dir-warn { margin: 0; font-size: var(--text-xs); color: var(--dash-ink); }

/* What a subscriber has FINISHED, and the control that records another (section 4
   item 4). Styled from the revoke disclosure above rather than invented: they sit in the
   same cell and reading as two different kinds of control would be noise. The ground is
   the neutral panel, not the warning tint — recording a completion gives somebody
   something, where ending access takes it away. */
.dir-done {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  font-size: var(--text-xs);
  color: var(--dash-ink-muted);
}
.dir-done li { margin-block-start: 2px; }
/* WITHDRAWN AND EARNED LOOK DIFFERENT (D-333) — the chip says which in words, and this
   is what stops the row reading as one more line of the same list. `--dash-ink-faint`
   rather than `--dash-ink-muted`: the comment at the top of this file measured it on
   every ground the dashboard paints, and it is the muted ink that clears AA on the
   tinted panel this list sits on. */
.dir-done .is-withdrawn {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-block-start: 5px;
  color: var(--dash-ink-faint);
}

.dir-done-add { margin-block-start: 6px; }
.dir-done-add > summary { list-style: none; cursor: pointer; }
.dir-done-add > summary::-webkit-details-marker { display: none; }
.dir-done-add form {
  display: grid; gap: var(--space-2);
  margin-block-start: 8px;
  padding: var(--space-3);
  border: 1px solid var(--dash-line);
  border-radius: var(--radius-sm);
  background: var(--dash-panel-2);
  min-inline-size: 15rem;
}

/* A countdown offer (PHASE F1): the same disclosure-and-panel shape as the
   completion recorder just above, including its neutral ground — giving somebody
   a discount is the same kind of act as recording what they finished, never the
   warning tint `.dir-revoke` wears for taking access away. */
.dir-countdown { margin-block-start: 6px; }
.dir-countdown > summary { list-style: none; cursor: pointer; }
.dir-countdown > summary::-webkit-details-marker { display: none; }
.dir-countdown-form {
  display: grid; gap: var(--space-2);
  margin-block-start: 8px;
  padding: var(--space-3);
  border: 1px solid var(--dash-line);
  border-radius: var(--radius-sm);
  background: var(--dash-panel-2);
  min-inline-size: 15rem;
}
.dir-countdown-form label {
  display: grid; gap: var(--space-1);
  font-size: var(--text-xs); font-weight: 700; color: var(--dash-ink-muted);
}
.dir-countdown-form input {
  font-variant-numeric: tabular-nums;
}

/* A gated nav link stays legible: it is still a real destination, and the page behind it
   explains itself. Greyed, not removed (D-226).
   
   Greyed with a COLOUR, not with opacity. At `opacity: .55` these measured 4.39:1 in light
   and 3.92:1 in dark — under AA, on the label for every feature a tenant's tier does not
   include, which is the upsell surface most tenants look at. --dash-ink-faint is a real
   token, measured on every ground this dashboard paints, and it reads as de-emphasised
   without going under the floor. */
/* `--dash-side-dim`, not `--dash-ink-faint`. The sidebar is dark navy in BOTH themes, and
   ink-faint is calibrated for a light page background: on the sidebar it measured 3.18:1, so
   replacing an opacity with the wrong token traded one AA failure for another. The sidebar has
   its own de-emphasis token, which is 4.92:1 on the tint these links actually paint on and
   above 5:1 on both sidebar grounds — and it is the token this text was always meant to use. */
.side-link.is-gated { color: var(--dash-side-dim); }
.side-link.is-gated:hover { color: var(--dash-side-ink); }
.side-gate { margin-inline-start: auto; font-size: var(--text-xs); line-height: 1; }
.locked-card.is-paused .locked-ico { font-size: var(--text-2xl); line-height: 1; }

/* `.note` is the neutral box the rest of the dashboard uses. These two are the
   outcome variants the keys page needs: a tenant who generated ten keys and one whose
   quota refused them must not be shown the same grey box. */
.note-ok {
  background: var(--dash-ok-soft);
  border-color: color-mix(in srgb, var(--dash-ok) 28%, transparent);
  color: var(--dash-ok-ink);
}
.note-bad {
  background: var(--dash-bad-soft);
  border-color: color-mix(in srgb, var(--dash-bad) 28%, transparent);
  color: var(--dash-bad-ink);
}

/* Secondary text under a value. */
.sub { color: var(--dash-ink-faint); font-size: var(--text-xs); line-height: 1.5; }
/* A block that needs air under it, for the two places a template would otherwise
   have reached for an inline margin. */
.pad-under { margin-block-end: 16px; }

/* The withdraw disclosure inside a subscriber row (D-302).
   
   A `.input` is `inline-size: 100%`, and inside a table cell with no width of its own that asks
   the TABLE for room — which a table grants, because a table grows to fit its content. The
   subscribers page went to 485px in a 390px viewport. So the disclosure gets a width that does
   not depend on the cell, and the field inside it is bounded by that rather than by the table. */
.dir-undo { display: block; max-inline-size: 16rem; }
.dir-undo form { display: flex; flex-direction: column; gap: var(--space-2);
                 margin-block-start: var(--space-2); }
.dir-undo .input { inline-size: 100%; min-inline-size: 0; }
.dir-done li { margin-block-end: var(--space-2); }
/* A sub-heading inside a card's padding. The only comparable rule was
   `.pack-card-head h3`, which is specific to pack cards. */
/* The state beside a pack: whether this tenancy actually has it. A chip rather than a
   sentence, because it is a state and not prose, and `is-on` is the only difference between
   the two readings — so they must not look the same. */
.pack-state {
  display: inline-flex; align-items: center;
  padding: var(--space-px) var(--space-2);
  border-radius: var(--radius-pill);
  border: 1px solid var(--dash-line);
  background: var(--dash-panel-2);
  color: var(--dash-ink-muted);
  font-size: var(--text-xs); font-weight: 700;
}
.pack-state.is-on {
  border-color: transparent;
  background: var(--dash-ok-soft);
  color: var(--dash-ok-ink);
}

/* A list that reads as sentences rather than as data: no disc, no hanging indent, the same
   rhythm as the paragraphs around it. */
.plain-list {
  margin: var(--space-2) 0 0;
  padding: 0;
  list-style: none;
}
.plain-list li {
  margin-block-end: var(--space-1);
  color: var(--dash-ink-muted);
  font-size: var(--text-sm); line-height: var(--leading);
}

.report-subhead {
  margin: var(--space-5) 0 var(--space-2);
  font-size: var(--text-md); font-weight: 800;
}
/* The action cell in a data table: the control sits at the row's end, where the eye
   already is after reading the numbers. Logical, not `right`, so RTL puts it on the
   other side without a second rule. */
.cell-end { text-align: end; }
.cell-end form { display: inline; }

/* ------------------------------------------------------------- access keys */
/* Shared by keys.html and discounts.html: both mint something and offer the
   same grid of short fields for it. Sits inside a `.pad`, same as every other
   card body, so it carries no padding of its own — only the grid itself. */
.keys-form {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: var(--space-4) var(--space-5);
}
.keys-form-actions { grid-column: 1 / -1; }
.keys-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--text-sm); letter-spacing: .06em;
  background: var(--dash-panel-2);
  border: 1px solid var(--dash-line-soft);
  border-radius: var(--radius-sm); padding: var(--space-px) var(--space-2);
  /* Always left-to-right: a key is a code, not prose, and an Arabic page must not
     reorder its characters. */
  direction: ltr; unicode-bidi: isolate; display: inline-block;
}
.keys-fresh-list { list-style: none; margin: 0 0 var(--space-2);
                   display: flex; flex-wrap: wrap; gap: var(--space-2); }
/* A spent or revoked key, dimmed by colour rather than by opacity: at `.6` it measured
   4.43:1 on the panel tint, and a key a tenant cannot read is a key they cannot check
   against the one a subscriber quotes at them. */
.keys-table tr.is-dead .keys-code { color: var(--dash-ink-faint); }

/* ===========================================================================
   FORM CONTROLS (D-269)

   Every setting on this page used to be the same text input, so a currency, a
   switch, a percentage and a welcome message all looked identical and none of
   them looked like what it was. These are the controls those values actually
   need, built from the scale in tokens.css so they line up with each other
   without per-field nudging.
   ======================================================================== */

.form-field {
  display: block;
  margin-block-end: var(--space-4);
}
.form-label {
  display: block;
  margin-block-end: var(--space-1);
  font-size: var(--text-sm);
  font-weight: 650;
  color: var(--dash-ink);
  line-height: var(--leading-snug);
}

/* Help text sits UNDER its control, not above: read in order, "what this is"
   then "how to fill it in" is the order someone needs it. */
.form-help {
  display: block;
  margin-block-start: var(--space-1);
  font-size: var(--text-xs);
  line-height: var(--leading);
  color: var(--dash-ink-muted);
}

/* The error replaces nothing -- it is added. A red border alone is not a
   message, and a screen reader announces no colour at all. */
.field-error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-1);
  margin-block-start: var(--space-1);
  font-size: var(--text-xs);
  line-height: var(--leading);
  font-weight: 600;
  color: var(--dash-bad-ink);
}
.field-error .dicon {
  inline-size: 14px;
  block-size: 14px;
  flex: none;
  margin-block-start: 1px;
}
.form-field.has-error .input,
.form-field.has-error .select {
  border-color: var(--dash-bad);
}
.form-field.has-error .input:focus-visible,
.form-field.has-error .select:focus-visible {
  box-shadow: var(--ring-danger);
}

/* One height for every control, from the token, so an input and a select in a
   row share a baseline. The select keeps its own arrow: a native one is the
   affordance people already know, and replacing it is how a select stops
   looking clickable. */
.input,
.select,
.textarea {
  inline-size: 100%;
  min-block-size: var(--control-h);
  padding: 0 var(--control-pad);
  border: 1px solid var(--dash-line);
  border-radius: var(--dash-radius-sm);
  background: var(--dash-panel);
  color: var(--dash-ink);
  font: inherit;
  font-size: var(--text-base);
}
.textarea {
  min-block-size: calc(var(--control-h) * 2.4);
  padding-block: var(--space-2);
  line-height: var(--leading);
  resize: vertical;
}
.select {
  /* appearance:none would remove the arrow and leave a box that reads as a text
     field. `padding-inline-end` makes room for the arrow the browser draws, and
     it is logical so it moves to the left in Arabic. */
  padding-inline-end: var(--space-8);
  cursor: pointer;
}

/* A unit that belongs to the number, not floating beside it. */
.input-group {
  display: flex;
  align-items: stretch;
}
.input-group .input {
  border-start-end-radius: 0;
  border-end-end-radius: 0;
}
.input-suffix {
  display: inline-flex;
  align-items: center;
  padding-inline: var(--space-3);
  border: 1px solid var(--dash-line);
  border-inline-start: 0;
  border-start-end-radius: var(--dash-radius-sm);
  border-end-end-radius: var(--dash-radius-sm);
  background: var(--dash-field);
  color: var(--dash-ink-muted);
  font-size: var(--text-sm);
  font-weight: 650;
  white-space: nowrap;
}

/* A switch with its label beside it, so the whole row is the hit target. */
.form-field-switch { margin-block-end: var(--space-4); }
.form-field-switch .switch {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-block-size: var(--control-h);
  cursor: pointer;
}
.switch-text {
  font-size: var(--text-base);
  font-weight: 650;
  color: var(--dash-ink);
}

/* The live preview of the welcome message, which is the only way to see what a
   subscriber will actually read before sending it to one. */
.preview {
  /* Zeroed explicitly, not left to the element: broadcast.html reuses this on
     a `<blockquote>`, whose UA stylesheet sets `margin: 1em 40px` — a `<div>`
     has no such default, which is why this went unnoticed until a second
     element used the class. */
  margin: var(--space-2) 0 0;
  padding: var(--space-3);
  border: 1px dashed var(--dash-line);
  border-radius: var(--dash-radius-sm);
  background: var(--dash-field);
  font-size: var(--text-sm);
  line-height: var(--leading);
  color: var(--dash-ink);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.preview:empty { display: none; }
.preview-label {
  display: block;
  margin-block-end: var(--space-1);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--dash-ink-faint);
}

/* The searchable dropdown, once dashboard.js has upgraded a <select>. Without
   JavaScript none of this applies and the native select is what shows. */
.combo { position: relative; }
.combo-input {
  inline-size: 100%;
  min-block-size: var(--control-h);
  padding: 0 var(--control-pad);
  border: 1px solid var(--dash-line);
  border-radius: var(--dash-radius-sm);
  background: var(--dash-panel);
  color: var(--dash-ink);
  font: inherit;
  font-size: var(--text-base);
  cursor: text;
}
.combo-input:focus-visible { box-shadow: var(--ring); border-color: var(--dash-brand); }
.combo-list {
  position: absolute;
  z-index: 40;
  inset-inline: 0;
  margin-block-start: var(--space-1);
  max-block-size: 16rem;
  overflow-y: auto;
  padding: var(--space-1);
  border: 1px solid var(--dash-line);
  border-radius: var(--dash-radius-sm);
  background: var(--dash-panel);
  box-shadow: var(--dash-shadow-lg);
}
.combo-list[hidden] { display: none; }
.combo-group {
  padding: var(--space-2) var(--space-2) var(--space-1);
  font-size: var(--text-xs);
  font-weight: 700;
  color: var(--dash-ink-faint);
  text-transform: uppercase;
  letter-spacing: .02em;
}
.combo-option {
  padding: var(--space-2) var(--space-2);
  border-radius: var(--dash-radius-sm);
  font-size: var(--text-base);
  color: var(--dash-ink);
  cursor: pointer;
}
.combo-option[aria-selected="true"] { font-weight: 700; }
.combo-option.is-active,
.combo-option:hover { background: var(--dash-brand-soft); color: var(--dash-brand-ink); }
.combo-empty {
  padding: var(--space-3) var(--space-2);
  font-size: var(--text-sm);
  color: var(--dash-ink-muted);
}

/* ===================================================== the sticky save bar */

/* ONE bar for the whole page (D-270). The Settings page had eight Save buttons --
   one per card -- so "have I saved?" had eight answers, and the one you wanted was
   whichever card you had scrolled past.

   Always visible rather than appearing on change, unlike /admin's: this page is
   mostly a form, and a bar that appears is a layout shift exactly when somebody is
   typing. The note beside it is what appears. */
.save-bar {
  position: sticky;
  inset-block-end: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-block-start: var(--space-6);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--dash-radius);
  background: var(--dash-side-bg);
  color: var(--dash-side-ink);
  box-shadow: var(--dash-shadow-lg);
}
.save-bar .spacer { flex: 1; }
.save-bar-note {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 650;
  color: var(--dash-side-ink);
}
.save-bar-note[hidden] { display: none; }

/* A button standing on the bar takes its ink from the BAR, not the page.
   `.btn-ghost` clears only the background, so Discard kept the page's dark ink and
   painted it on a dark bar: the label was there and invisible, an outline with
   nothing in it. /admin hit this exactly once and the fix is the same one. Scoped
   to the bar, because `.btn-ghost` must keep reading as dark ink on light panels
   everywhere else. */
.save-bar .btn-ghost {
  color: var(--dash-side-ink);
  border-color: var(--dash-side-line);
  background: none;
}
.save-bar .btn-ghost:hover {
  background: rgba(255, 255, 255, .12);
  color: var(--dash-on-brand);
}

@media (max-width: 720px) {
  /* The note moves above the buttons rather than squeezing them: on a phone the
     two buttons need their full width to stay tappable. */
  .save-bar { flex-wrap: wrap; }
  .save-bar-note { flex-basis: 100%; }
}


/* ===========================================================================
   A REFERRAL PAYOUT REQUEST (§4 item 3)

   One request is a handful of labelled facts and two decisions, not a table row:
   the wallet alone is 34–44 characters and would either squeeze every other
   column or push the table sideways on a phone. So it is a definition list,
   which is what a list of label/value pairs is, and the two decisions sit under
   it with the reason the refusal needs.
   ======================================================================== */
.payout-request { margin-block-end: var(--space-4); }
.payout-request:last-child { margin-block-end: 0; }

.payout-who {
  margin: 0 0 var(--space-3);
  font-size: var(--text-md);
  font-weight: 800;
}
.payout-who .sub { font-weight: 500; margin-inline-start: var(--space-2); }

/* Two columns on anything wider than a phone, one on a phone. `minmax(0, …)` on the
   value column so a 44-character address wraps inside it instead of widening the grid
   and scrolling the page sideways. */
.payout-facts {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: var(--space-2) var(--space-4);
  margin: 0 0 var(--space-3);
}
.payout-facts dt {
  color: var(--dash-ink-muted);
  font-size: var(--text-sm);
  font-weight: 650;
}
.payout-facts dd { margin: 0; min-inline-size: 0; }
.payout-facts dd .keys-code { max-inline-size: 100%; word-break: break-all; }

@media (max-width: 560px) {
  /* Stacked: at 390px a `max-content` label column leaves the address nowhere to go. */
  .payout-facts { grid-template-columns: minmax(0, 1fr); gap: var(--space-px); }
  .payout-facts dd { margin-block-end: var(--space-2); }
}


/* -----------------------------------------------------------------------------
   The language picker, themed for this shell.

   `langpick.css` defaults to the public palette. This shell has its own dark
   theme in the `--dash-*` namespace and does NOT redefine `--surface` or
   `--line`, so without these four the menu would paint white with a pale border
   on a dark page. Scoped to `.langpick` so it cannot reach anything else.
   -------------------------------------------------------------------------- */
/* BOTH VARIANTS. `_langpicker.html` has two macros: `picker()` wraps itself in
   `.langpick`, and `inline()` renders a bare `.langpick-inline` for the pages with
   no room for a popover (login, status, locked_gate). Scoping this to `.langpick`
   alone left the inline list inheriting the PUBLIC `--ink` on this shell's dark
   panel, measured at 1.00:1 — invisible — on /dashboard/status. */
.langpick,
.langpick-inline {
  --langpick-bg:        var(--dash-panel);
  --langpick-line:      var(--dash-line);
  --langpick-ink:       var(--dash-ink);
  --langpick-ink-muted: var(--dash-ink-muted);
}

/* -----------------------------------------------------------------------------
   Collapsible sections, themed for this shell. `accordion.css` defaults to the
   public palette; this shell has its own dark theme in the `--dash-*` namespace
   and does not redefine `--ink` or `--line`.
   -------------------------------------------------------------------------- */
.acc {
  --acc-ink:       var(--dash-ink);
  --acc-ink-muted: var(--dash-ink-muted);
  --acc-line:      var(--dash-line);
  --acc-hover:     var(--dash-hover, rgba(0, 0, 0, .03));
}

/* =============================================================================
   /earn — the marketer's page
   =============================================================================
   Four rules and no new layout system. The surface reuses `.dash-main`,
   `.dash-top`, `.card`, `.tiles` and `.data` as they are, which is the whole
   reason it looks like the rest of the product; what it genuinely needs that
   nothing else does is a link people copy, and a thirty-bar chart. */

/* `.dash-main` is a grid COLUMN inside `.dash-shell` on the tenant surface. Here
   it is the whole page, so it needs the width constraint the shell was giving
   it — without this, the tables run to the edge of a 27" monitor. */
.earn-main { min-block-size: 100vh; }

/* The link itself. `user-select: all` so one click selects the whole URL: the
   copy button needs script, and the most common thing somebody does when a
   button does nothing is select the text by hand. */
.earn-url {
  user-select: all;
  word-break: break-all;
  font-size: var(--text-md);
}

.earn-utm { margin-block-start: var(--space-5); }
/* Three short fields, side by side where there is room and stacked where there is
   not. `.field-row` was the first attempt and it is the wrong component: it is a
   flex row with no wrap, built for the plans table's two inline edits, so at 390px
   the three inputs ran off the screen. A grid with a minimum column width is the
   thing that cannot do that. */
.earn-utm-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--space-3);
}

/* THE CHART. Thirty bars, flex, no library — the CSP allows no CDN and thirty
   numbers do not need one. The table underneath is not a fallback bolted on: it
   is the accessible version of the same data, and the bars are aria-hidden
   decoration over it. */
.earn-chart {
  display: flex;
  align-items: flex-end;
  gap: var(--space-px);
  block-size: 140px;
  margin-block: var(--space-4);
  padding-block-end: 2px;
  border-block-end: 1px solid var(--dash-line);
}
.earn-bar-col {
  flex: 1 1 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  block-size: 100%;
  min-inline-size: 3px;
}
.earn-bar {
  inline-size: 100%;
  /* A day with ONE visitor still has to be visibly a bar. A 1px sliver beside an
     empty day is indistinguishable from nothing having happened, which is the
     opposite of what the chart is for. */
  min-block-size: 2px;
  /* Token-exempt: see the design-token-usage test for why these two
     geometry-driven corner radii are allowed to stay raw px. */
  border-radius: 3px 3px 0 0;
  background: var(--dash-accent, var(--blue-600));
}
/* The numbers, for anybody who wants them rather than the shape. Capped so a
   quiet month does not push the sections below it off the screen. */
.earn-chart-table { max-block-size: 320px; overflow: auto; display: block; }

/* =============================================================================
   The account menu, and the gate pages' legal line
   =============================================================================
   `_account_nav.html` is SHARED with the public site's header, and its five
   classes were defined only in tgaccess.css — which this shell deliberately does
   not link (see the header of this file). So on every dashboard page the avatar
   was an un-circled letter and the popover had no background, no border and no
   shadow: a menu painted straight onto whatever was behind it. Same story for
   `.gate-legal` on the dashboard's own login page.

   Nothing noticed because `test_every_class_a_dashboard_template_uses_is_defined_
   somewhere` had two regexes written with doubled backslashes, so it matched
   nothing and passed by checking nothing. The guard is fixed; these are what it
   found.

   Themed in the --dash-* namespace rather than copied: the public values are
   `--surface` and `--line`, which this shell does not define. */

.site-account-btn { gap: var(--space-2); padding-inline-start: 6px; }

.site-account-initial {
  inline-size: 28px;
  block-size: 28px;
  border-radius: 50%;
  flex: none;
  display: grid;
  place-items: center;
  background: var(--dash-accent);
  color: var(--dash-on-brand);
  font-weight: 800;
  font-size: 12px;
  text-transform: uppercase;
}

.site-account-pop {
  border: 1px solid var(--dash-line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  background: var(--dash-panel);
  color: var(--dash-ink);
  padding: var(--space-2);
  min-inline-size: 230px;
  font: inherit;
}

.site-account-who {
  margin: 0 0 var(--space-2);
  padding: var(--space-1) var(--space-3) var(--space-3);
  border-block-end: 1px solid var(--dash-line);
  font-size: 12.5px;
  color: var(--dash-ink-muted);
}

/* A menu reads as a list: every row's label starts at the same place. */
.site-account-pop .btn-block { justify-content: flex-start; }
.site-account-pop form { margin: 0; }

/* Hidden on a phone on the public site, and for the same reason here: the
   header has room for the account button or for secondary links, not both. */
@media (max-width: 860px) {
  .nav-secondary { display: none; }
}

.gate-legal {
  margin-block-start: 14px;
  font-size: .82rem;
  line-height: 1.6;
  color: var(--dash-ink-muted);
}

/* The wizard's help lists, used by /earn's marketing kit. Defined here for the
   same reason as the five above: tgaccess.css is not linked on this surface. */
/* THE BOT, SHOWN BACK on the verification step: its own name and picture, read from
   Telegram the moment the token was accepted. Logical properties throughout, so the
   picture sits on the right in Arabic without a second rule. */
.bot-face {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-3); margin-block-end: 16px;
  border: 1px solid var(--dash-line); border-radius: var(--radius-sm);
  background: var(--dash-panel-2);
}
.bot-face-photo {
  inline-size: 48px; block-size: 48px; border-radius: 50%;
  object-fit: cover; flex: none;
}
/* The fallback when the bot has no picture — same circle, so the card does not change
   shape depending on whether Telegram had one. */
.bot-face-blank {
  display: grid; place-items: center; font-size: 24px;
  background: var(--dash-bg);
}
.bot-face-text { display: flex; flex-direction: column; gap: var(--space-px); min-inline-size: 0; }
.bot-face-text > * { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.steps-help { color: var(--dash-ink-muted); padding-inline-start: 1.3em; }
.steps-help li { margin-block-end: 9px; }

/* A column of controls with a consistent gap. `.stack-top` existed and `.stack`
   did not, which is a trap: the name reads as though it would work. */
.stack { display: grid; gap: var(--space-4); }

/* The link beside its copy button. `.pay-row` is invoices.html's name for the
   same arrangement, which was ALSO undefined — the amount and its button were
   held together by nothing but source order. */
.pay-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

/* =============================================================== /dashboard/tier

   The plan cards. Everything else on the page is `.card`, `.chip`, `.summary-row`
   and `.steps-help` as they are; what is new is a price that has to be the
   biggest thing in the card and a current-plan card that has to be obviously
   the one you are on. */

.tier-card { display: flex; flex-direction: column; }
/* The chosen one, marked by more than a chip: a border nobody has to read. */
.tier-card.is-current { outline: 2px solid var(--dash-accent); }

/* `.summary-row` renders the commission-percent/code-quota pair on each card — but this
   page's only styling for it lived in `tgaccess.css`, the public site's stylesheet,
   which `_base.html`'s own header comment says dashboard pages deliberately do not
   load. With no grid rule at all, a bare `<dl>` falls back to the browser default: `dd`
   is a block element with its own `margin-inline-start`, so each value rendered on its
   own line, indented under its label, rather than beside it. Defined here rather than
   re-adding the public stylesheet, for the same reason that comment gives — this is the
   one place on the whole dashboard that used a public-site class and happened to work
   by accident until a tenant actually looked at a quota number. */
.summary-row { display: grid; grid-template-columns: auto 1fr; gap: var(--space-2) var(--space-5); margin: 0; }
.summary-row dt { color: var(--dash-ink-muted); font-size: .88rem; }
.summary-row dd { margin: 0; font-weight: 600; }

/* The pending-request card's "Pay now" is a `<summary>` wearing `.btn` classes, open by
   default, so the payment panel is already visible the moment the card renders rather
   than behind a second tap — same `<details>`-as-disclosure pattern `.dir-revoke` and
   `.dir-done-add` already use elsewhere on this dashboard, just defaulting open instead
   of closed. `list-style: none` and the marker rules are what stop the native triangle
   fighting the button look `.btn` already gives it. */
.pay-disclosure > summary { list-style: none; cursor: pointer; margin-block-end: var(--space-3); }
.pay-disclosure > summary::-webkit-details-marker { display: none; }
.pay-disclosure .pay-panel { margin-block-start: var(--space-3); }

.tier-price {
  font-size: var(--text-2xl);
  font-weight: 800;
  letter-spacing: -.02em;
  margin: 0 0 var(--space-4);
}
.tier-per { font-size: var(--text-sm); font-weight: 600; color: var(--dash-ink-muted); }

.tier-diff { margin-block-start: var(--space-4); }
.tier-diff-label {
  display: block;
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--dash-ink-muted);
  margin-block-end: 6px;
}

/* The cards stretch to the tallest in the row, so their prices and buttons line
   up instead of floating at whatever height their feature list ends. */
.dash-grid-cards > .tier-card > .pad { flex: 1; display: flex; flex-direction: column; }
.dash-grid-cards > .tier-card form,
.dash-grid-cards > .tier-card > .pad > p.lede:last-child { margin-block-start: auto; }

/* A code, an id, a hash: fixed-width, left-to-right whatever the page direction, and
   selectable in one gesture because the next thing anybody does with one is copy it.
   /admin calls this `.hash`; this shell had no name for it at all, and the marketer
   page was using /admin's — a class defined in a stylesheet it does not link. */
.hash {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--text-sm);
  direction: ltr;
  unicode-bidi: isolate;
  user-select: all;
}

/* The tier somebody was SENT here for (O16). Marked more loudly than the current
   one, because they followed a link that promised this specific card — and with a
   chip beside it saying why, since a highlight nobody can explain reads as a bug
   to anyone who arrived another way. */
.tier-card.is-wanted {
  outline: 2px solid var(--dash-brand, var(--dash-accent));
  outline-offset: 2px;
}

/* Proof of payment in the approval queue (G5 / S11).

   A THUMBNAIL AND A LABEL, both inside the one link. The thumbnail alone is not
   enough — what the tenant needs off a receipt is an amount and an address, neither
   of which is legible at 40px — so the picture is the recognition cue and the link is
   what actually opens it. Fixed `block-size` on the image rather than `height: auto`:
   the proxy serves whatever aspect ratio the subscriber's phone produced, and a row
   that resizes when a tall screenshot finally loads is a row somebody mis-clicks.

   `object-fit: cover` crops rather than squashes, because a squashed screenshot is
   unreadable in a way a cropped one is not. */
.pay-proof {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  text-decoration: none;
  color: inherit;
  font-size: var(--text-sm);
}

.pay-proof img {
  inline-size: 40px;
  block-size: 40px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  border: 1px solid var(--dash-line);
  /* A broken image — a proof Telegram has expired, a token that stopped decrypting —
     must not collapse to a bare alt string jammed against the label. The background
     keeps the box the same size either way. */
  background: var(--dash-line-soft);
}

.pay-proof:hover img, .pay-proof:focus-visible img {
  border-color: var(--dash-brand, var(--dash-accent));
}

.pay-proof-none { color: var(--dash-ink-faint); font-size: var(--text-sm); }

/* The "English version prevails" line on the 18+ addendum page (D-490). Same job and name as the
   public legal pages'. */
.legal-english-prevails {
  margin: var(--space-3) 0;
  font-size: var(--text-sm);
  line-height: 1.55;
  color: var(--dash-ink-muted);
  background: var(--dash-panel-2);
  border: 1px solid var(--dash-line-soft);
  border-radius: var(--dash-radius-sm);
  padding: var(--space-3);
}

/* D-500: a language list that grows must never widen the page. Six languages
   already measured 441px on a 390px viewport; at ten no header could hold the row.
   The row may be as wide as it likes INSIDE its own box: it takes the full line when
   it no longer fits beside the title (the bar wraps), and scrolls sideways within
   itself past that. Generic in the number of languages, and in the translated
   names, so adding a language needs no CSS. */
.seg {
  max-inline-size: 100%;
  min-inline-size: 0;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
}
.seg a, .seg button { white-space: nowrap; flex: none; }
.dash-top .seg { flex: 0 1 auto; }

/* ===================================================== THE TAIL PROBE =====
   KEEP THIS RULE LAST. tests/test_dashboard_pages_layout.py measures it in a real
   browser to prove the whole stylesheet parsed: a stray `}` anywhere above silently
   drops every rule after it, and a parser that counts braces cannot see that because
   it does the same arithmetic the bug does. Anything appended below this line is
   outside the guard — put new rules above it. */
/* Two controls side by side inside a cell or a paragraph, with no form margin to
   fight. Same name and same job as /admin's. */
.inline-form { display: inline-flex; align-items: center; gap: var(--space-2); margin: 0; }
