/* ==========================================================================
   console.css — the control panel, end to end.

   ## Two folders, one design system

   Decided 2026-09-15: the marketing site and the control panel are two
   products that happen to share a server, and they stopped sharing a
   stylesheet because a change to one kept being a change to the other.

       public/site/     the site. site.css, the fonts, the marks, the page.
       public/console/  this. Served at /console/*.

   **That split still holds; what changed on 2026-09-19 is the palette it
   holds apart.** The DoConure rebrand rewrote the site and stopped at this
   door, leaving a green front page handing over to an indigo console — and
   the owner closed the seam the same day: *puoi mettere lo stesso template e
   colori anche nella gestione admin/tenant? così siamo allineati*.

   So the two files still share nothing mechanically — no import, no token
   read across the folder boundary — and now agree on every value. That is
   deliberate rather than lazy: an application and a marketing page want
   different density, different shadows and different type sizes, and
   coupling them again to keep the greens in step would bring back the
   original problem. `test_palette_contrast.py` is what keeps them in step
   instead: it reads both files and fails if a shared name drifts.

   The one thing this folder does reach for is `/assets/fonts.css` — the
   self-hosted Inter and Manrope. A typeface is a brand asset, not a layout
   decision, and serving a second 169 KB copy of the same four woff2 files
   from this folder would be waste with no upside.

   ## The look

   Structure and density are the Materialize admin template the owner picked
   as the reference, measured off its own stylesheet. The colour is
   DoConure's:

     green    #2b8217 for a fill, #1f6b12 for a word, on #f1f6f0
     cards    pure white, **no border**, soft shadow, 10px radius
     controls 6px radius, 500 weight, a coloured shadow under the primary one
     menu     the current entry is a filled pill, not an underline
     text     15px Inter, body #33424e, headings #17232d in Manrope
     tables   no vertical rules, a tinted header, hover on rows
     status   a soft tint with a dark text pair, never a saturated block

   The indigo is gone. So are the serif, the oxblood and the paper stock
   before it — see .claude/kb/00-overview/brand.md for what each of them was.

   ## Contents
     1. tokens
     2. base and typography
     3. the shell and its rails
     4. the signed-in account
     5. page furniture: heads, cards, tables
     6. controls
     7. the auth pages
     8. shared pieces: kebab, avatar, navmenu, org
     9. help
    10. responsive
   ========================================================================== */

/* ==========================================================================
   1. Tokens
   ========================================================================== */
:root {
  /* --- brand ------------------------------------------------------------
   * The same two-green split the site uses, one step further. `--primary`
   * fills a button; `--primary-text` is any green *word*. The identity
   * green (#46b92d) appears nowhere here at all: it is 2.55:1 on white and
   * this file has no logotype to put it in.
   *
   * The split is needed twice over in an application, because a green link
   * lands on four different grounds — a white card, a hovered row, a table
   * header, a selected pill — and #2b8217 is 4.87:1 on white, which leaves
   * nothing for a tint to eat. #1f6b12 is 5.51:1 on the darkest of them. */
  --primary: #2b8217;
  --primary-text: #1f6b12;
  --primary-dark: #1f6b12;
  --primary-soft: #dcf0d6;
  --primary-shadow: rgba(43, 130, 23, .30);

  /* --- status. Every pair is a tint plus a text colour that reads on it,
   * and every one of them is a DoConure colour: there is no blue in this
   * palette, so "info" is the deep green rather than an invented hue. */
  --success: #2b8217; --ok-bg: #dcf0d6; --ok-fg: #1d5c11;
  --info:    #087a3e; --info-bg: #e8f4ed; --info-fg: #0b5c30;
  --warning: #f4c915; --warn-bg: #fdf7e0; --warn-fg: #8a6a00;
  /* The warm feather red at #ee4b2f is 3.70:1 under white text — fine on an
   * icon, not under the word "Delete". The fill is one step darker. */
  --danger:  #c93a1f; --err-bg: #fdf1ee; --err-fg: #8c2a15;

  /* --- surfaces ---------------------------------------------------------
   * `--sunken` is *inside* a white card (table header, hovered menu item);
   * `--bg` is the page behind the cards. So sunken is the paler of the two,
   * which looks backwards from the names and is what keeps a green word on
   * a hovered row at 5.87:1. */
  --bg: #f1f6f0;
  --bg-soft: #f8fbf8;
  --surface: #ffffff;
  /* #eaf4e7 was one hundredth under: `--warn-fg` landed at 4.49:1 on it.
   * Two shades lighter, and every text token clears AA on every ground
   * this file defines. */
  --sunken: #eef6ec;
  --border: #e4e9e6;
  --border-strong: #d3ddd7;

  /* --- text ------------------------------------------------------------- */
  --heading: #17232d;
  --body: #33424e;
  --muted: #5f6d7a;
  /* Disabled controls and placeholder text only. WCAG exempts a disabled
   * control from contrast; nothing that has to be read may use this. */
  --faint: #8794a0;

  /* The operator's frame, so our console is never mistaken for a
     customer's. The brand charcoal, not black — black beside white cards
     reads as a hole punched in the page. */
  --menu-dark: #17232d;
  --menu-dark-fg: #ffffff;

  /* --- shape ------------------------------------------------------------ */
  --radius: 10px;
  --radius-sm: 6px;
  --shadow-sm: 0 1px 3px rgba(23, 35, 45, .07);
  --shadow: 0 2px 10px rgba(23, 35, 45, .10);
  --shadow-lg: 0 6px 20px rgba(23, 35, 45, .14);

  /* --- type -------------------------------------------------------------
   * Inter for everything, Manrope for headings — the site's pair, served
   * from /assets/fonts.css. Until 2026-09-19 this was the platform sans,
   * on a no-web-fonts rule that was really a no-third-party-request rule;
   * self-hosting satisfies it, and the console had been the odd one out. */
  --font: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;
  --font-display: 'Manrope', 'Inter', -apple-system, "Segoe UI", Roboto,
                  sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Consolas,
               "Liberation Mono", monospace;

  /* --- spacing ---------------------------------------------------------- */
  --sp-1: 0.25rem; --sp-2: 0.5rem;  --sp-3: 0.75rem;
  --sp-4: 1rem;    --sp-5: 1.5rem;  --sp-6: 2rem;
  --sp-7: 3rem;    --sp-8: 4.5rem;
  --measure: 72ch;
}

/* ==========================================================================
   2. Base and typography
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }

body.app {
  margin: 0;
  background: var(--bg);
  color: var(--body);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

.app h1, .app h2, .app h3, .app h4 {
  color: var(--heading);
  font-family: var(--font);
  line-height: 1.3;
}
.app h1, .app h2, .app h3 { font-family: var(--font-display); }
.app h1 { font-size: 1.5rem; font-weight: 700; letter-spacing: -.022em; }
.app h2 { font-size: 1.1rem; font-weight: 700; letter-spacing: -.015em; }
.app h3 { font-size: .95rem; font-weight: 700; letter-spacing: -.01em; }
/* A link *in the page* is indigo and underlines on hover. Scoped to plain
   links inside the content column on purpose: `.app a` used to say this, and
   at specificity (0,1,1) it beat every component that styles its own anchors
   — which is how the rail's entries turned blue and grew an underline. A link
   that carries a class dresses itself.

   The `:where()` matters. Plain `a:not([class])` would raise this to (0,2,1)
   and win against `.quick a` in turn, painting a filled button's label the
   same indigo as its background — an invisible button, which is exactly what
   happened. `:where()` contributes nothing, so this stays at (0,1,1) and
   every component declared after it keeps the last word. */
.console-main a:where(:not([class])) { color: var(--primary-text); text-decoration: none; }
.console-main a:where(:not([class])):hover { text-decoration: underline; }
/* A link inside running text is underlined, not told apart by colour alone
   (WCAG 1.4.1, axe `link-in-text-block`; the public site and legal pages got
   the same rule on 2026-10-06, the console on RC3 when axe first saw it; a checkbox label
   with a link to terms since run4, 2026-10-27; a link in a .notice since rc13-night5, 2026-10-30). */
.console-main :is(p, li, dd, td, th, label, label > span, .muted, .lead, small, .notice) > a:where(:not([class])) { text-decoration: underline; }
.app p { margin: 0 0 1em; }
.app ul, .app ol { margin: 0 0 1em; padding-left: 1.3em; }
.app hr { border: 0; border-top: 1px solid var(--border); margin: var(--sp-5) 0; }

.muted { color: var(--muted); font-size: .88em; }
.eyebrow {
  font-size: .68rem; text-transform: uppercase; letter-spacing: .08em;
  color: var(--muted); font-weight: 600;
}
code {
  background: var(--sunken); padding: .15em .45em; border-radius: 4px;
  font-family: var(--font-mono); font-size: .86em; color: var(--heading);
}
.prose { max-width: var(--measure); }

/* ==========================================================================
   3. Shell
   ========================================================================== */

/* A row: zero, one or two rails, then the page. The rails are sticky rather
   than fixed, so the document still scrolls normally — a scroll container per
   pane is how an app shell ends up with two scrollbars and a header that will
   not print. */
.console { display: flex; align-items: stretch; min-height: 100vh; background: var(--bg); }

.console-main {
  flex: 1 1 auto;
  min-width: 0;                      /* or a wide table stretches the layout */
  padding: var(--sp-5) var(--sp-6) var(--sp-8);
}
.console-main--reading { max-width: 82ch; }

/* --- a rail --------------------------------------------------------------
   One component, two skins. The tokens below are what the operator's rail and
   the customer's disagree about; every rule after them is shared. */
.rail {
  --rail-w: 15.5rem;
  --rail-bg: var(--surface);
  --rail-fg: #5f6d7a;
  --rail-fg-strong: var(--heading);
  --rail-hover: var(--sunken);
  --rail-edge: var(--border);
  --rail-current: var(--tenant-accent, var(--primary));

  flex: 0 0 var(--rail-w);
  width: var(--rail-w);
  background: var(--rail-bg);
  box-shadow: var(--shadow-sm);
  position: sticky;
  top: 0;
  align-self: flex-start;
  height: 100vh;
  display: flex;
  flex-direction: column;
  z-index: 10;
}

/* Ours. Dark, so at a glance you can tell you are inside the operator's
   console and not a customer's — the same thing the banner spells out. */
.rail--admin {
  --rail-w: 13.5rem;
  --rail-bg: var(--menu-dark);
  --rail-fg: var(--menu-dark-fg);
  --rail-fg-strong: #fff;
  --rail-hover: rgba(255,255,255,.07);
  --rail-edge: rgba(255,255,255,.10);
  --rail-current: var(--primary);
}

.rail.is-mini { --rail-w: 4.375rem; }

/* --- the logo, always top-left ------------------------------------------ */
.rail__brand {
  display: flex; align-items: center; gap: .65em;
  min-height: 4rem;
  padding: var(--sp-4) var(--sp-4) var(--sp-3);
  color: var(--rail-fg-strong);
  text-decoration: none;
  font-family: var(--font-display);
  font-size: 1.15rem; font-weight: 800; letter-spacing: -.028em;
  line-height: 1.15; overflow: hidden;
}
.rail__brand,
.rail__brand:link,
.rail__brand:visited,
.rail__brand:hover { color: var(--rail-fg-strong); text-decoration: none; }
.rail__brand img { height: 26px; width: auto; max-width: 100%; display: block; flex: 0 0 auto; }
.rail__brand .mark-class {
  color: var(--primary); font-weight: 700; font-size: .72em; letter-spacing: .06em;
}
.rail--admin .rail__brand .mark-class { color: #8fd66f; }
/* The customer's own mark gets room to be itself; ours does not need any. */
.rail__brand--tenant { min-height: 4.6rem; }
.rail__brand--tenant img { height: 38px; max-width: 10rem; }
/* No `text-transform: uppercase` here, and that is not a style choice:
   this slot holds the product name, and `DOCONURE` is not how the name is
   written. The capital C is where the wordmark's two colours meet. */
.rail__brand-sub {
  display: block; font-size: .68rem; letter-spacing: .01em;
  color: var(--muted); font-weight: 600;
}

/* --- entries ------------------------------------------------------------- */
.rail__nav {
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--sp-2) var(--sp-3) var(--sp-3);
  overflow-y: auto; flex: 1 1 auto;
}
.rail__section {
  font-size: .66rem; letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted); font-weight: 600;
  padding: var(--sp-4) var(--sp-2) .35em;
}
.rail--admin .rail__section { color: rgba(255,255,255,.65); }

/* The current entry is a filled pill — the single most recognisable gesture
   of the reference, and the right one here: a vertical list has no baseline
   to underline against. */
.rail__item {
  display: flex; align-items: center; gap: .8em;
  padding: .58em .85em;
  border-radius: var(--radius-sm);
  color: var(--rail-fg);
  text-decoration: none;
  font-size: .9rem;
  white-space: nowrap;
  transition: background-color .15s ease, color .15s ease;
}
.rail a,
.rail a:link,
.rail a:visited { color: var(--rail-fg); text-decoration: none; }
.rail a:hover,
.rail a:focus,
.rail a:active { text-decoration: none; }

/* Hover and focus change the ground, never the ink: the text stays the same
   white it always is, and what moves is the surface behind it. */
.rail__item:hover,
.rail__item:focus { background: var(--rail-hover); color: var(--rail-fg-strong); }
.rail__item[aria-current="page"],
.rail__item[aria-current="page"]:link,
.rail__item[aria-current="page"]:visited,
.rail__item[aria-current="page"]:hover,
.rail__item[aria-current="page"]:focus { color: #fff; }
.rail__item[aria-current="page"] {
  background: var(--rail-current);
  color: #fff;
  font-weight: 500;
  box-shadow: 0 2px 6px color-mix(in srgb, var(--rail-current) 45%, transparent);
}
.rail__item[aria-current="page"]:hover { color: #fff; }
.rail__item .ic { flex: 0 0 auto; opacity: .95; }

/* Collapsed: icons only. The label stays in the DOM as the accessible name
   and as the native tooltip, so nothing is lost but width. */
.rail.is-mini .rail__label,
.rail.is-mini .rail__section,
.rail.is-mini .rail__brand span,
.rail.is-mini .rail__chip-text { display: none; }
.rail.is-mini .rail__item,
.rail.is-mini .rail__brand,
.rail.is-mini .rail__chip { justify-content: center; padding-left: 0; padding-right: 0; }

/* --- the collapse toggle ------------------------------------------------- */
.rail__toggle {
  background: none; border: 0; cursor: pointer; box-shadow: none;
  color: var(--rail-fg); padding: .5em .85em;
  margin: 0 var(--sp-3) var(--sp-2);
  border-radius: var(--radius-sm); font-size: .8rem; font-family: var(--font);
  display: flex; align-items: center; gap: .8em;
}
.rail__toggle:hover { background: var(--rail-hover); color: var(--rail-fg-strong); }
.rail.is-mini .rail__toggle { justify-content: center; margin-left: var(--sp-2); margin-right: var(--sp-2); }
.rail.is-mini .rail__toggle .ic { transform: scaleX(-1); }

/* ==========================================================================
   4. The signed-in account, always bottom-left
   ========================================================================== */
.rail__foot { margin-top: auto; border-top: 1px solid var(--rail-edge); padding: var(--sp-3); }

/* A link, not a menu. One click to the page that manages the account, where
   Sign out also lives. */
.rail__chip {
  display: flex; align-items: center; gap: .7em;
  padding: .5em .55em; border-radius: var(--radius-sm);
  color: var(--rail-fg); font-size: .84rem; overflow: hidden;
  text-decoration: none;
}
.rail__chip,
.rail__chip:link,
.rail__chip:visited { color: var(--rail-fg); text-decoration: none; }
.rail__chip:hover,
.rail__chip:focus {
  background: var(--rail-hover); color: var(--rail-fg-strong); text-decoration: none;
}
.rail__chip-text { min-width: 0; line-height: 1.3; }
.rail__chip-text strong {
  display: block; font-weight: 600; font-size: .86rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ==========================================================================
   5. Page furniture
   ========================================================================== */

/* The banner naming the organisation an operator is acting on. Loud on
   purpose: acting on the wrong organisation is the most expensive mistake
   this console permits, and a collapsed rail must not make it cheaper. */
.acting {
  background: var(--warn-bg); color: var(--warn-fg);
  padding: var(--sp-3) var(--sp-6);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4); flex-wrap: wrap; font-size: .84rem;
}
.acting strong { font-size: .95rem; }
.acting a { color: var(--warn-fg); text-decoration: underline; }
.console-main > .acting:first-child {
  margin: calc(var(--sp-5) * -1) calc(var(--sp-6) * -1) var(--sp-5);
}

.page-head, .t-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--sp-4); flex-wrap: wrap; margin-bottom: var(--sp-5);
}
.page-head h1, .t-head h1 { margin: 0 0 .15em; }
.page-head__actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; align-items: center; }
.t-head__note, .page-head__note { color: var(--muted); font-size: .88rem; margin: 0; }

/* Cards float. No border: the reference separates with a shadow, and a border
   plus a shadow is two solutions to one problem. */
.card, .panel {
  background: var(--surface); border: 0; border-radius: var(--radius);
  box-shadow: var(--shadow); padding: var(--sp-5); margin-bottom: var(--sp-5);
}
.card > h2, .panel > h2, .card > h3:first-child { margin-top: 0; }

.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: var(--sp-4); }
.card-stat {
  background: var(--surface); border: 0; border-radius: var(--radius);
  box-shadow: var(--shadow); padding: var(--sp-4) var(--sp-5);
}
.card-stat__label {
  font-size: .72rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted); font-weight: 600;
}
.card-stat__value {
  font-family: var(--font-display);
  font-size: 1.75rem; font-weight: 700; line-height: 1.2; letter-spacing: -.02em;
  margin-top: .15em; color: var(--heading); letter-spacing: -.01em;
}
.card-stat__note { font-size: .78rem; color: var(--muted); margin-top: .2em; }

.quick { display: flex; gap: var(--sp-3); flex-wrap: wrap; }
.quick a {
  display: inline-flex; align-items: center; gap: .5em;
  background: var(--primary); color: #fff; text-decoration: none;
  padding: .58em 1.2em; border-radius: var(--radius-sm);
  font-size: .88rem; font-weight: 500;
  box-shadow: 0 2px 4px var(--primary-shadow);
}
.quick a:hover { background: var(--primary-dark); color: #fff; text-decoration: none; }
.quick a.ghost {
  background: transparent; color: var(--primary-text);
  border: 1px solid color-mix(in srgb, var(--primary) 50%, #fff); box-shadow: none;
}
.quick a.ghost:hover { background: var(--primary-soft); color: var(--primary-dark); }
.filters { margin-bottom: var(--sp-4); }
.quick a.ghost.is-current { background: var(--primary-soft); color: var(--primary-dark); font-weight: 600; }

/* Rows separated by one hairline, which is all a row needs.

   **No `overflow: hidden` here, and no radius.** They were there to round the
   corners, and they were clipping every `⋮` menu in every table on every page:
   an absolutely positioned popup cannot escape an element that hides its
   overflow, whatever z-index it carries. The rounding belongs to the wrapper,
   which is also the thing that knows whether it is allowed to clip. */
table { border-collapse: collapse; width: 100%; background: var(--surface); }
th, td {
  border-bottom: 1px solid var(--border);
  padding: .75em .95em; text-align: left; font-size: .88rem; vertical-align: middle;
}
th {
  background: var(--sunken); color: var(--heading);
  font-weight: 600; font-size: .72rem; letter-spacing: .06em; text-transform: uppercase;
}
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover td { background: var(--bg-soft); }
.table-wrap {
  overflow-x: auto; background: var(--surface);
  border-radius: var(--radius); box-shadow: var(--shadow); margin-bottom: var(--sp-5);
}
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }

.badge {
  display: inline-block; padding: .3em .75em; border-radius: 50rem;
  font-size: .68rem; font-weight: 600; letter-spacing: .03em; text-transform: uppercase;
}
.badge.active, .badge.succeeded, .badge.ok { background: var(--ok-bg); color: var(--ok-fg); }
.badge.inactive, .badge.off { background: #eef2ef; color: var(--muted); }
.badge.failed, .badge.danger { background: var(--err-bg); color: var(--err-fg); }
.badge.test, .badge.gaps, .badge.warn { background: var(--warn-bg); color: var(--warn-fg); }
/* A test row is red and capitalised everywhere (1.1): nobody mistakes it for production. */
.badge.test.is-test { background: #c62828; color: #fff; font-weight: 700; letter-spacing: .04em; }

/* ==========================================================================
   6. Controls
   ========================================================================== */
button, input[type=submit] {
  background: var(--primary); color: #fff; border: none;
  padding: .58em 1.25em; border-radius: var(--radius-sm); cursor: pointer;
  font-family: var(--font); font-size: .86rem; font-weight: 500; letter-spacing: .01em;
  box-shadow: 0 2px 4px var(--primary-shadow);
  transition: background-color .15s ease, box-shadow .15s ease;
}
button:hover { background: var(--primary-dark); }
button.secondary {
  background: transparent; color: var(--body);
  border: 1px solid var(--border-strong); box-shadow: none;
}
button.secondary:hover { background: var(--sunken); color: var(--heading); }
button.danger { background: var(--danger); box-shadow: 0 2px 4px rgba(201, 58, 31, .35); }
button.danger:hover { background: #a82e17; }
button[disabled] { background: var(--border); color: var(--faint); cursor: not-allowed; box-shadow: none; }
/* Same pair as the public site: the mark is decorative, `aria-required` is
   the fact. */
.req { color: var(--danger, #b3261e); font-weight: 700; }
.opt { color: var(--faint); font-weight: 400; font-size: .88em; }
form.inline { display: inline; }

a.button-link {
  display: inline-block; background: var(--primary); color: #fff;
  text-decoration: none; padding: .58em 1.25em; border-radius: var(--radius-sm);
  font-size: .86rem; font-weight: 500; box-shadow: 0 2px 4px var(--primary-shadow);
}
a.button-link:hover { background: var(--primary-dark); color: #fff; text-decoration: none; }

input, select, textarea {
  padding: .62em .8em; border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); font-size: .9rem; width: 100%;
  font-family: var(--font); background: var(--surface); color: var(--heading);
  transition: border-color .15s ease, box-shadow .15s ease;
}
input:focus, select:focus, textarea:focus {
  /* No `outline: none` here (RC3 closure review): it outranked the
     `:where(...)` keyboard ring below, so form controls had only this 16 %
     halo (about 1.15:1) - near invisible on a checkbox. */
  border-color: var(--primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 16%, transparent);
}
input::placeholder, textarea::placeholder { color: var(--faint); }
textarea { font-family: var(--font-mono); font-size: .85em; line-height: 1.55; }
label {
  display: block; margin-bottom: .45em; font-weight: 500; font-size: .82rem;
  color: var(--heading);
}
.field { margin-bottom: var(--sp-4); }
.field-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); gap: var(--sp-4); }

/* Soft tint, dark text, no bar down the side. */
.notice, .error, .success {
  padding: .85em 1.15em; border-radius: var(--radius-sm);
  margin-bottom: var(--sp-4); font-size: .88rem; border: 0;
}
.notice { background: var(--sunken); color: var(--body); }
.notice.error, .error { background: var(--err-bg); color: var(--err-fg); }
.notice.ok, .success { background: var(--ok-bg); color: var(--ok-fg); }
.notice.warn { background: var(--warn-bg); color: var(--warn-fg); }
.notice.info { background: var(--info-bg); color: var(--info-fg); }

/* An icon. Sized in em, so it follows whatever text it sits next to. */
.ic {
  width: 1.2em; height: 1.2em; fill: none; stroke: currentColor;
  stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round;
}

/* ==========================================================================
   7. (was the auth pages)

   Deleted 2026-09-19. Sign in, forgotten password and choose-a-password
   moved onto the site's stylesheet, through the shared
   `auth_templates/_auth.html`: they are the pages somebody sees before
   they are a customer, so they belong to the front door rather than to
   the room behind it. Nothing here rendered them any more.
   ========================================================================== */

/* ==========================================================================
   8. The shared pieces (_shared/ui.html)
   ========================================================================== */

/* A row menu. Native <details>, so no script. Used only where a row has two
   or more actions — hiding a single action behind a menu turns one click into
   two, which is the opposite of light. */
.kebab { position: relative; display: inline-block; }
.kebab > summary {
  list-style: none; cursor: pointer; user-select: none;
  width: 1.9em; height: 1.9em; line-height: 1.9em; text-align: center;
  border-radius: var(--radius-sm); color: var(--muted); font-size: 1.05em;
}
.kebab > summary::-webkit-details-marker { display: none; }
.kebab > summary:hover,
.kebab[open] > summary { background: var(--sunken); color: var(--heading); }
.kebab__menu {
  position: absolute; right: 0; top: 2.1em; z-index: 40; min-width: 12rem;
  background: var(--surface); border-radius: var(--radius);
  box-shadow: var(--shadow-lg); padding: .35em 0; text-align: left;
}
.kebab__menu form { display: block; margin: 0; }
.kebab__item {
  display: block; width: 100%; text-align: left;
  background: none; border: 0; box-shadow: none; color: var(--body);
  padding: .55em 1.05em; font-size: .86rem; cursor: pointer;
  text-decoration: none; font-family: var(--font); font-weight: 400; border-radius: 0;
}
.kebab__item:hover { background: var(--sunken); color: var(--primary-text); text-decoration: none; }
.kebab__item--danger { color: var(--danger); }
.kebab__item--danger:hover { background: var(--err-bg); color: var(--err-fg); }

/* Avatars are always a circle and always square, so a column of them lines up
   whether or not anyone has uploaded a picture. */
.avatar {
  border-radius: 50%; object-fit: cover; display: inline-block;
  vertical-align: middle; flex: 0 0 auto; background: var(--primary-soft);
}
.avatar--initials {
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 600; color: var(--primary-text); letter-spacing: .02em; font-family: var(--font);
}
.avatar-link { text-decoration: none; display: inline-flex; }

/* A dropdown inside a page. */
.navmenu { position: relative; display: inline-flex; align-items: stretch; }
.navmenu > summary {
  list-style: none; cursor: pointer; display: flex; align-items: center;
  padding: .4em var(--sp-3); color: var(--body); font-size: .86rem;
  border-radius: var(--radius-sm);
}
.navmenu > summary::-webkit-details-marker { display: none; }
.navmenu > summary:hover, .navmenu[open] > summary { background: var(--sunken); }
.navmenu__menu {
  position: absolute; left: 0; top: calc(100% + .3em); z-index: 50; min-width: 12rem;
  background: var(--surface); border-radius: var(--radius);
  box-shadow: var(--shadow-lg); padding: .35em 0;
}
.navmenu__menu a {
  display: block; padding: .6em 1.05em; font-size: .86rem;
  color: var(--body); text-decoration: none;
}
.navmenu__menu a:hover { background: var(--sunken); color: var(--primary-text); text-decoration: none; }
.navmenu--right .navmenu__menu { left: auto; right: 0; }
.navmenu__head { padding: .75em 1.05em; font-size: .8rem; line-height: 1.35; }
.navmenu__head strong { display: block; font-size: .88rem; color: var(--heading); }
.navmenu__head span { color: var(--muted); word-break: break-all; }

/* An organisation accordion. The body is fetched the first time it opens —
   see the script at the foot of organisations.html. */
.org {
  border: 0; border-radius: var(--radius); background: var(--surface);
  box-shadow: var(--shadow); margin-bottom: var(--sp-3);
}
.org > summary {
  list-style: none; cursor: pointer; padding: .9em var(--sp-5);
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  border-radius: var(--radius);
}
.org > summary::-webkit-details-marker { display: none; }
.org > summary::before { content: "\25B8"; color: var(--muted); }
.org[open] > summary::before { content: "\25BE"; }
.org > summary:hover { background: var(--sunken); }
.org__name { font-weight: 600; color: var(--heading); }
.org__meta { color: var(--muted); font-size: .8rem; }
.org__body { padding: 0 var(--sp-5) var(--sp-5); }
.org__loading { color: var(--muted); font-size: .85rem; padding: .6em 0; }
.org--ours { box-shadow: var(--shadow), inset 3px 0 0 var(--primary); }

/* ==========================================================================
   9. Help
   ==========================================================================
   Numbered steps first, depth folded under them.
   See admin_templates/help/_howto.html. */
ol.howto { margin: 0; padding-left: 1.4em; }
ol.howto > li { margin-bottom: var(--sp-4); padding-left: .3em; line-height: 1.6; }
ol.howto > li::marker { color: var(--primary-text); font-weight: 700; }
.howto__note {
  display: block; margin-top: .4em;
  color: var(--muted); font-size: .86rem; line-height: 1.5;
  border-left: 2px solid var(--border); padding-left: .85em;
}

details.deeper { padding: 0; }
details.deeper > summary {
  list-style: none; cursor: pointer;
  padding: var(--sp-4) var(--sp-5);
  display: flex; align-items: center; gap: .6em;
  color: var(--heading); border-radius: var(--radius);
}
details.deeper > summary::-webkit-details-marker { display: none; }
details.deeper > summary::before { content: "\25B8"; color: var(--primary); }
details.deeper[open] > summary::before { content: "\25BE"; }
details.deeper > summary:hover { background: var(--sunken); }
.deeper__body {
  padding: var(--sp-4) var(--sp-5) var(--sp-5);
  border-top: 1px solid var(--border);
}
.deeper__body > :first-child { margin-top: 0; }
.deeper__body > :last-child { margin-bottom: 0; }
.deeper__body p, .deeper__body li { line-height: 1.6; max-width: var(--measure); }

.card pre {
  background: var(--menu-dark); color: #e6ebe8;
  padding: var(--sp-4) var(--sp-5); border-radius: var(--radius-sm);
  overflow-x: auto; font-size: .84rem; line-height: 1.6;
  font-family: var(--font-mono);
}
.card pre code { background: none; padding: 0; color: inherit; font-size: inherit; }

/* ==========================================================================
   10. Responsive
   ==========================================================================
   Below the breakpoint the rails stop being columns and become a strip along
   the top: at phone width a 15rem rail leaves nothing for the page, and a rail
   that *is* the page is a menu — which is what this turns into. */
@media (max-width: 900px) {
  .console { flex-direction: column; }
  .rail {
    --rail-w: auto;
    /* `width: auto` on a row flex container resolves to max-content, so the
       strip grew to fit all nine entries — 1026px of it at 390px viewport —
       and `overflow-x: auto` on this element could not contain what was
       setting its own width. The scroll belongs on `.rail__nav` below; this
       one just has to stay inside the page. Pre-dates the 2026-09-19
       retheme and was found while checking it. */
    flex: 0 0 auto; width: 100%; max-width: 100%; min-width: 0; height: auto;
    position: static; flex-direction: row; align-items: center;
    overflow-x: clip;
  }
  .rail__brand { min-height: 0; padding: var(--sp-3); }
  .rail__brand--tenant img { height: 26px; }
  .rail__nav { flex-direction: row; overflow-x: auto; padding: var(--sp-2);
               min-width: 0; flex: 1 1 auto; }
  .rail__section, .rail__toggle { display: none; }
  .rail__foot { margin-top: 0; margin-left: auto; border-top: 0; padding: 0 var(--sp-2);
                flex: 0 0 auto; }
  /* Three things competing for 390px, resolved by dropping the two that
     repeat themselves elsewhere. The brand keeps its mark and loses its
     words — the page's own <h1> already names the organisation, and a
     wordmark truncated to "D..." tells nobody anything; a tenant that has
     uploaded no logo has no mark either, so the whole link goes and the
     nav's own Home entry carries it. The account becomes its avatar: the
     name is one tap away on the page it links to. The nav takes all the
     slack and scrolls. */
  .rail__brand { flex: 0 0 auto; min-width: 0; padding-right: var(--sp-2); }
  .rail__brand > span { display: none; }
  .rail__brand:not(:has(img)) { display: none; }
  .rail__chip { padding: .35em; }
  .rail__chip-text { display: none; }
  /* A collapsed rail that became a strip keeps the pill around its icon: the column rule above drops
     the side padding to centre the icon, and in a row that squeezed the green pill to the icon's
     own width (owner's staging review, 10-27; seen at 800px with the rail collapsed). */
  .rail.is-mini .rail__item { padding-left: .6em; padding-right: .6em; }
  .console-main { padding: var(--sp-4) var(--sp-4) var(--sp-8); }
  .acting { padding: var(--sp-3) var(--sp-4); }
  .console-main > .acting:first-child {
    margin: calc(var(--sp-4) * -1) calc(var(--sp-4) * -1) var(--sp-4);
  }
}

@media print {
  .rail, .acting, .page-head__actions { display: none !important; }
  .console-main { padding: 0; }
  .card, .panel, .table-wrap { box-shadow: none; border: 1px solid var(--border); }
}

/* --- a row's actions ------------------------------------------------------
   Small caps rather than sentence case: they are controls, not prose, and a
   column of them reads as a column. */
.row-actions { white-space: nowrap; text-align: right; }
.row-actions a {
  display: inline-block; margin-left: var(--sp-3);
  font-size: .72rem; font-weight: 600; letter-spacing: .06em;
}
.row-actions a:first-child { margin-left: 0; }

/* A popup inside a scrolling table gets clipped by the scroll container:
   `overflow-x: auto` makes a box a scroll container in *both* directions, so
   an absolutely positioned menu disappears under the card's own edge.

   `:has()` lifts the clip while a menu is open, which is the only moment
   anything needs to escape — but it is a modern selector and it is not worth
   betting a row's actions on. So a table whose rows carry a `⋮` uses
   `.table-open` instead and simply is not a scroll container, at the width
   where it does not need to be. */
.table-wrap:has(details[open]) { overflow: visible; }

.table-open {
  background: var(--surface); border-radius: var(--radius);
}
@media (max-width: 900px) {
  /* Narrow enough that scrolling matters more than a popup that fits. */
  .table-open { overflow-x: auto; }
}

/* --- buying credit -------------------------------------------------------
   The pack cards on /tenant/billing/credits. A card is a form, so the whole
   thing is one submit target and there is no separate "select then confirm"
   step: choosing a pack *is* opening an order, which is then a page of its
   own that can be abandoned safely.

   No new colours. Every value is a token this file already defines, so the
   contrast test has already measured all of them.
   ---------------------------------------------------------------------- */
.pack-grid {
  display: grid; gap: var(--sp-4); margin-bottom: var(--sp-5);
  grid-template-columns: 1fr;
}
@media (min-width: 640px)  { .pack-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1100px) { .pack-grid { grid-template-columns: repeat(3, 1fr); } }

.pack-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--sp-5);
  display: flex; flex-direction: column; margin: 0;
}
.pack-card--featured { border-color: var(--primary); }
.pack-card__tag {
  align-self: flex-start; background: var(--primary-soft); color: var(--primary-text);
  border-radius: 999px; padding: .15em .7em;
  font-size: .68rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; margin: 0 0 var(--sp-3);
}
.pack-card h2 { margin: 0 0 var(--sp-2); font-size: 1.05rem; }
.pack-card__price {
  margin: 0 0 var(--sp-2); font-size: 1.6rem; font-weight: 800;
  letter-spacing: -.02em; color: var(--heading);
}
.pack-card__price span {
  font-size: .72rem; font-weight: 600; color: var(--muted); margin-left: .4em;
}
.pack-card__credits { margin: 0; font-weight: 600; color: var(--primary-text); }
.pack-card__docs { margin: 0; color: var(--muted); font-size: .84rem; }
.pack-card__blurb {
  margin: var(--sp-3) 0 var(--sp-4); color: var(--body);
  font-size: .88rem; flex: 1;
}
.pack-card__gross { margin: 0 0 var(--sp-4); color: var(--muted); font-size: .76rem; }
.pack-card button { width: 100%; }

/* The three simulated outcomes sit side by side on a desktop and stack on a
   phone. `.row-actions` is right-aligned and nowrap, which is right for a
   table row and wrong for three buttons in a panel. */
.panel .row-actions {
  display: flex; flex-wrap: wrap; gap: var(--sp-3);
  text-align: left; white-space: normal; margin-top: var(--sp-4);
}
.panel .row-actions form { margin: 0; }

/* --- Template Studio ---------------------------------------------------
   The field map, and the language tabs over the generated snippets. The
   tabs are radio inputs and not JavaScript, the same as on the signup page:
   this is the panel somebody copies a command out of, and it has to work
   with scripting off. `.snips__radio` is hidden with opacity rather than
   `display:none`, which would take it out of the tab order and make the
   control unreachable from a keyboard. */
.fieldmap {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(14rem, 100%), 1fr));
  gap: .25em .8em; margin: .4em 0 0; padding: 0; list-style: none;
}
.fieldmap li { font-size: .88rem; padding: .15em 0 }
.studio__sub { font-size: .95rem; margin-top: var(--sp-4); margin-bottom: 0 }
.studio__prompt {
  width: 100%; resize: vertical; font-family: var(--font-mono, monospace);
  font-size: .78rem; line-height: 1.5; margin-top: .6em;
}
/* `.snips` is the radios' containing block, and a radio takes one pixel: positioned
   against nothing, each radio took the global `width: 100%` of the *viewport* from where
   it stood, and the template page and the welcome page scrolled sideways on every phone
   (2026-10-21, the e2e's overflow audit). */
.snips { margin-block: var(--sp-3); position: relative }
.snips__radio {
  position: absolute; top: 0; left: 0; width: 1px; height: 1px; margin: 0; padding: 0;
  opacity: 0; pointer-events: none;
}
/* The radios are the control and are invisible, so the keyboard focus is
   drawn on the tab label (mirrors public/site/assets/site.css; RC3 review). */
.snips__radio:focus-visible + .snips__tabs label[data-for],
.snips__tabs label:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px }
.snips__tabs {
  display: flex; flex-wrap: wrap; gap: .3em;
  border-bottom: 1px solid var(--border); margin-bottom: var(--sp-3);
}
.snips__tabs label {
  font-size: .82rem; font-weight: 600; color: var(--muted);
  padding: .4em .75em; border-radius: var(--radius) var(--radius) 0 0;
  cursor: pointer; border: 1px solid transparent; border-bottom: 0;
  margin-bottom: -1px;
}
.snips__tabs label:hover { color: var(--ink) }
.snips__panel { display: none }
.snips__panel code { white-space: pre }
pre.snips__panel {
  overflow-x: auto; white-space: pre; font-size: .78rem; line-height: 1.55;
}

/* --- What is left -------------------------------------------------------
   Sits above the account chip, so it is the last thing in the rail before
   the person. `is-low` and `is-empty` are two steps rather than one: the
   difference between "top up soon" and "the next call fails" is the whole
   of what somebody needs from this, and one colour cannot say both.
   Hidden in the mini rail, where there is no room for a number and a label
   and half of either is worse than neither — the `title` still carries it. */
.rail__credit {
  display: flex; align-items: baseline; gap: .5em;
  margin: var(--sp-4) var(--sp-3) 0; padding: .6em .75em;
  border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--ink); text-decoration: none; background: var(--surface);
}
a.rail__credit:hover { border-color: var(--border-strong) }
.rail__credit-n { font-size: 1.35rem; font-weight: 700; line-height: 1 }
.rail__credit-l { font-size: .74rem; color: var(--muted); line-height: 1.25 }
.rail__credit-sub { display: block }  /* no opacity: 4.5:1 failed at .8 (axe, RC3) */
.rail__credit.is-low { border-color: #d8a33a }
.rail__credit.is-low .rail__credit-n { color: #8a6412 }
.rail__credit.is-empty { border-color: #c0544a }
.rail__credit.is-empty .rail__credit-n { color: #96382f }
.rail.is-mini .rail__credit { display: none }

/* --- topping up --------------------------------------------------------
   One amount, four suggestions. The chips are `type="button"` so a click
   never submits: they fill the box in, and the box is the only thing the
   form sends. */
.topup__suggest { display: flex; gap: .5em; flex-wrap: wrap; margin: var(--sp-4) 0 }
.topup__chip { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.2 }
.topup__chip span { font-size: .72rem; font-weight: 400; opacity: .75 }
.topup__amount { max-width: 16rem }
.topup__amount input { font-size: 1.25rem; font-weight: 600 }

/* --- a condition on the top-up form ------------------------------------
   One row, drawn like the signup form's consent rows because it is the
   same kind of thing: a declaration somebody makes, next to the button
   that acts on it. It appears only for a consumer - a professional never
   had the right it gives up. */
.consent__row {
  display: flex; gap: .7em; align-items: flex-start;
  margin: var(--sp-4) 0 var(--sp-4); cursor: pointer; max-width: 46rem;
}
.consent__row input[type=checkbox] {
  flex: 0 0 auto; width: 1.05rem; height: 1.05rem; margin-top: .18rem;
  accent-color: var(--brand, #46b92d);
}
.consent__row > span { font-size: .85rem; line-height: 1.55; color: var(--ink-2, var(--muted)) }

/* Skip link (RC3, WCAG 2.4.1): the first stop of the keyboard, off-screen until
   focused, then over everything. The rail has a dozen links before the page. */
.skip {
  position: absolute; left: -9999px; top: 0; z-index: 1000;
  background: var(--ink); color: #fff; padding: .6em 1em;
  border-radius: 0 0 var(--radius) 0; font-weight: 600;
}
.skip:focus { left: 0; color: #fff; text-decoration: none; }

/* Words for a screen reader only (2026-10-18): the header of an actions or avatar
   column has nothing to show, and an empty <th> is announced as nothing at all
   (axe `empty-table-header`). Clipped, not displayed:none, so it is still read. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* Its box is positioned against the cell, so it stays inside the table's own scrolling
   wrapper: positioned against nothing it escaped the wrapper and widened the page to the
   table's width on a phone (the e2e's 320-pixel check, 2026-10-18). */
th:has(> .sr-only) { position: relative; }

/* Keyboard focus, everywhere, the same way (RC3, WCAG 2.4.7 / 1.4.11): a 2px ring
   in the primary green (4.6:1 on white) outside the element, white on the dark
   rail. Inputs keep their border change for the mouse and get the ring for the
   keyboard. test/e2e/test_keyboard_and_semantics.py walks every page with Tab
   and fails on a focused element that shows no ring. */
:where(a, button, summary, select, input, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--primary); outline-offset: 2px;
}
.rail :where(a, button, summary, [tabindex]):focus-visible { outline-color: #fff; }

/* Reflow (RC3, WCAG 1.4.10): a control never wider than its column. A file
   input at `width: auto` takes its intrinsic ~330 px, which pushed the profile
   and settings pages sideways on a 320 px phone. */
input, select, textarea { max-width: 100%; }
/* A fold's header is a control: at least 24 px tall for every pointer (WCAG 2.2 2.5.8).
   "JSON Schema (optional)" on a template page measured 23, 4 px from the file input, on a
   768 px tablet where the touch rules below did not apply (e2e, 2026-10-21). Low weight
   on purpose: the kebab, fold and tree headers keep their own sizes, the touch rules
   still make it 44. */
main details > summary { min-height: 24px; box-sizing: border-box; }
/* A file input's minimum content width (button + "No file chosen") still held
   a flex row open; it takes its column instead, and flex children may shrink.
   `!important` only because the `u-30e741d` utility sets `width: auto !important`. */
@media (max-width: 640px) {
  input[type=file] { width: 100% !important; min-width: 0 !important; }
}
.u-865301d > * { min-width: 0; }

/* One switch of the operator's products page (RC4): the box on the left, the words
   beside it. Without this the global `input` width put every checkbox on a line of its own. */
.flag {
  display: flex; gap: .7em; align-items: flex-start;
  margin: .75em 0; max-width: 46rem; cursor: pointer;
}
.flag input[type=checkbox] {
  flex: 0 0 auto; width: 1.05rem; height: 1.05rem; margin-top: .2rem;
  accent-color: var(--brand, #46b92d);
}
.flag__body { display: block; font-weight: normal; }
.flag__body strong { font-weight: 600; }
.flag__body .muted { display: block; font-size: .85rem; margin-top: .15rem; }

/* The admin note (RC4): the text area takes the whole width of its card and the button
   sits under it, on the right, with room above. */
.admin-note textarea { display: block; width: 100%; max-width: none; min-height: 7rem; box-sizing: border-box; resize: vertical; }
.admin-note__actions { display: flex; justify-content: flex-end; margin-top: var(--sp-3, .9rem); }

/* The permissions of a new API key (1.1): one fieldset per scope group, a checkbox each
   (`.flag` draws the box and the words). */
.key-scopes { border: 1px solid var(--line, #d9d9d9); border-radius: .5rem; margin: 0 0 var(--sp-3, .9rem); padding: .4rem 1rem .6rem; }
.key-scopes legend { font-weight: 600; padding: 0 .4rem; }

/* A long operator page in folding sections (1.1, `/admin/tenants/{id}`, owner
   2026-10-14): each section a white card that opens on its title, the blocks
   inside it white cards too, text across the whole width, every Save at the
   bottom right. Works without script; `ui.js` only opens the section an
   address's #anchor points into. */
details.fold { padding: 0; }
details.fold > summary {
  list-style: none; cursor: pointer;
  display: flex; align-items: baseline; gap: .6em; flex-wrap: wrap;
  padding: var(--sp-4) var(--sp-5); border-radius: var(--radius);
}
details.fold > summary::-webkit-details-marker { display: none; }
details.fold > summary::before { content: "\25B8"; color: var(--primary); }
details.fold[open] > summary::before { content: "\25BE"; }
details.fold > summary:hover { background: var(--sunken); }
details.fold > summary h2 { display: inline; margin: 0; font-size: 1.05rem; }
.fold__hint { margin-left: auto; color: var(--muted); font-size: .82rem; }
.fold__body {
  padding: var(--sp-4) var(--sp-5) var(--sp-5);
  border-top: 1px solid var(--border);
}
.fold__body > :last-child { margin-bottom: 0; }
.fold__body p, .fold__body .muted { max-width: none; }
.sub {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: var(--sp-4) var(--sp-5);
  margin: 0 0 var(--sp-4);
}
.sub > h3:first-child { margin-top: 0; }
.sub > :last-child { margin-bottom: 0; }
.form-actions {
  display: flex; flex-direction: row-reverse; justify-content: flex-start; flex-wrap: wrap;
  gap: var(--sp-2); margin-top: var(--sp-4);
}
/* Label, input and the words after it on one line on a desktop, stacked on a
   phone. The input is kept short: these are numbers and notes, not essays. */
.field-line {
  display: grid; grid-template-columns: minmax(12rem, 20rem) minmax(7rem, 16rem) 1fr;
  gap: var(--sp-3); align-items: center; margin-bottom: var(--sp-3);
}
.field-line > label { margin: 0; }
.field-line > input, .field-line > select { width: 100%; }
.field-line > .muted { font-size: .82rem; }
@media (max-width: 640px) {
  .field-line { grid-template-columns: 1fr; gap: .35em; }
}

/* One search box over the guide, the answers and the help (1.1): in the guide's
   own nav and at the top of both console rails. `/console/js/guide-search.js`. */
.guide-search { position: relative; margin-bottom: .8em; }
.guide-search input {
  width: 100%; box-sizing: border-box; padding: .5em .7em;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  font-size: .88em; background: var(--bg, #fff);
}
.guide-search__results {
  position: absolute; z-index: 20; top: calc(100% + .3em); left: 0; right: 0;
  background: var(--surface); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg, var(--shadow)); max-height: 60vh;
  overflow-y: auto; padding: .3em;
}
.guide-search__hit {
  display: block; padding: .5em .6em; border-radius: var(--radius-sm);
  text-decoration: none; color: inherit;
}
.guide-search__hit:hover { background: var(--primary-soft); }
.guide-search__hit strong { display: block; font-size: .9em; color: var(--heading, inherit); }
.guide-search__heading { font-size: .78em; color: var(--primary); }
.guide-search__snippet {
  display: block; font-size: .78em; color: var(--muted);
  margin-top: .15em; line-height: 1.4;
}
.guide-search__empty { padding: .6em; font-size: .85em; color: var(--muted); margin: 0; }
.guide-search__kind {
  float: right; font-size: .68rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted); margin-left: .5em;
}
.rail .guide-search { margin: 0 var(--sp-3, .75rem) var(--sp-3, .75rem); }
.rail .guide-search__results { min-width: 18rem; }
.rail.is-mini .guide-search { display: none; }

/* Maintenance (1.1): the courtesy page anybody but an operator sees, and the banner on
   every console page while it is about to start or on. */
.maintenance-page {
  max-width: 40rem; margin: 10vh auto; padding: var(--sp-6, 2rem) var(--sp-5, 1.5rem);
  background: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow);
}
.maintenance-page h1 { font-size: 1.4rem; margin: var(--sp-4, 1rem) 0 .4em; }
.maintenance-page h1[lang="en"] { margin-top: var(--sp-6, 2rem); }
.maintenance-banner [data-countdown] { font-variant-numeric: tabular-nums; }
.maintenance-state { font-size: 1.1rem; }

/* ==========================================================================
   Release 1.2 - owner's review of staging, 2026-10-20
   ========================================================================== */

/* The line that names the zone of every time on the page. Inside <main> (it was a sibling
   of the rail and became a column of its own on the right). */
.zone-note { margin: var(--sp-6) 0 0; text-align: right; }

/* The rail's own scrollbar, in the colours of the rail (the native light bar on a dark
   rail was the one thing on the page that was not ours). */
.rail__nav { scrollbar-width: thin; scrollbar-color: rgba(0,0,0,.25) transparent; }
.rail--admin .rail__nav { scrollbar-color: rgba(255,255,255,.28) transparent; }
.rail__nav::-webkit-scrollbar { width: 8px; }
.rail__nav::-webkit-scrollbar-track { background: transparent; }
.rail__nav::-webkit-scrollbar-thumb { background: rgba(0,0,0,.25); border-radius: 8px; }
.rail--admin .rail__nav::-webkit-scrollbar-thumb { background: rgba(255,255,255,.28); }

/* One shape for a page's head: the title and note on the left, the primary action all the
   way right, on every list page. The note may shrink so the button never drops under it. */
.page-head > div:first-child, .t-head > div:first-child { flex: 1 1 24rem; min-width: 0; }
.page-head__actions, .t-head__actions { margin-left: auto; flex: 0 0 auto; display: flex;
  gap: var(--sp-2); flex-wrap: wrap; align-items: center; justify-content: flex-end; }
.t-head .quick { margin-left: auto; flex: 0 0 auto; justify-content: flex-end; }

/* The cards of a row keep clear of what follows them. */
.cards { margin-bottom: var(--sp-5); }

/* --- an action that is an icon ---------------------------------------------
   The words are the accessible name (aria-label) and a tooltip that appears after
   three seconds of hover, at once on keyboard focus. Touch has no hover: no tooltip. */
.icon-action {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 2.1rem; height: 2.1rem; border-radius: var(--radius); margin-left: var(--sp-1);
  color: var(--heading); background: none; border: 0; cursor: pointer; padding: 0;
}
.icon-action--go { color: var(--primary); }
.icon-action:hover, .icon-action:focus-visible { background: var(--ok-bg); }
.icon-action .ic { width: 1.25rem; height: 1.25rem; }
.icon-action::after {
  content: attr(data-tip); position: absolute; right: 0; bottom: calc(100% + 6px);
  white-space: nowrap; background: var(--menu-dark); color: #fff; font-size: .72rem;
  font-weight: 500; letter-spacing: 0; text-transform: none; padding: .35em .65em;
  border-radius: 6px; opacity: 0; visibility: hidden; pointer-events: none; z-index: 40;
  transition: opacity .15s ease, visibility 0s linear .15s;
}
.icon-action:hover::after { opacity: 1; visibility: visible; transition-delay: 3s; }
.icon-action:focus-visible::after { opacity: 1; visibility: visible; transition-delay: 0s; }
.table-wrap:has(.icon-action:hover, .icon-action:focus-visible) { overflow: visible; }
@media (hover: none) { .icon-action::after { display: none; } }

/* --- a table that sorts ----------------------------------------------------
   `sortable.js` turns each header of a `table.sortable` into a button; the arrow shows
   the direction, `aria-sort` says it to a screen reader. */
th.sortable-th { padding: 0; }
th.sortable-th > button {
  all: unset; box-sizing: border-box; display: flex; align-items: center; gap: .4em;
  width: 100%; padding: .75em .95em; cursor: pointer; color: inherit; font: inherit;
  letter-spacing: inherit; text-transform: inherit;
}
th.sortable-th.num > button { justify-content: flex-end; }
th.sortable-th > button:hover { background: var(--bg-soft); }
th.sortable-th > button:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.sort-arrow { display: inline-block; width: .7em; opacity: .35; font-size: .8em; }
.sort-arrow::before { content: "\2195"; }
th[aria-sort="ascending"] .sort-arrow, th[aria-sort="descending"] .sort-arrow { opacity: 1; color: var(--primary); }
th[aria-sort="ascending"] .sort-arrow::before { content: "\25B2"; }
th[aria-sort="descending"] .sort-arrow::before { content: "\25BC"; }

/* --- forms: label left, field right ---------------------------------------
   A form of one field per row reads label | control when there is room, and stacked (label
   above) on a narrow screen. Multi-column grids (`.field-row`) keep their label above.
   Opted into by every customer-console form of one field per row (owner, 2026-10-20):
   Settings, the replace-signer form, the profile, API keys, users, name and colour.
   `max-width: none` is !important because a field may carry a width utility (`u-656270c`,
   22rem, is !important too) that would squeeze the control into what the label leaves. */
@media (min-width: 720px) {
  .form-rows .field:not(:has(input[type="checkbox"], input[type="radio"])) {
    display: grid; grid-template-columns: 13rem minmax(0, 1fr); column-gap: var(--sp-5);
    align-items: start; max-width: none !important;
  }
  .form-rows .field > label { padding-top: .55em; font-weight: 600; }
  .form-rows .field > :not(label) { grid-column: 2; }
  .form-rows .field > input, .form-rows .field > select, .form-rows .field > textarea { max-width: 34rem; }
  .form-rows .field .muted { max-width: 46rem; }
  .form-rows .field-row .field:not(:has(input[type="checkbox"], input[type="radio"])) { display: block; }
  .form-rows .field-row .field > label { padding-top: 0; }
}

/* --- one filter row: the buttons sit on the baseline of the fields --------- */
.filter-bar { align-items: flex-end; }
.filter-bar .field { margin-bottom: 0; }
.filter-bar button { margin-bottom: 0; }
.export-under { display: flex; justify-content: flex-end; margin: calc(var(--sp-4) * -1) 0 var(--sp-5); }

/* --- the Limits page: every section on the same columns ---------------------
   Separate tables line up because the columns are fixed, not sized by their content. */
table.limits-table { table-layout: fixed; width: 100%; }
table.limits-table th { width: 17rem; }
table.limits-table td:nth-child(2) { width: 9rem; }
@media (max-width: 720px) {
  table.limits-table th { width: 40%; }
  table.limits-table td:nth-child(2) { width: 22%; }
}

/* The search lives at the top of the Help page (1.2), wide enough to read what it finds. */
.help-search { margin-bottom: var(--sp-5); max-width: 40rem; }
.help-search .guide-search { margin-bottom: 0; }
.help-search .guide-search input { font-size: 1rem; padding: .7em 1em; border-radius: var(--radius); }

/* --- the architecture picture (1.2): shapes styled here, never inline (CSP) ------------- */
.arch { margin: var(--sp-4) 0 0; }
.arch__svg { width: 100%; height: auto; display: block; }
.arch__line { fill: none; stroke: var(--muted, #6b7a73); stroke-width: 1.6; }
.arch__head { fill: var(--muted, #6b7a73); }
.arch__node rect { fill: var(--surface, #fff); stroke: var(--border, #cfd8d2); stroke-width: 1.4; }
.arch__node--core rect { fill: var(--primary-soft, #e8f3e4); stroke: var(--primary, #2a8416); }
.arch__node--edge rect { fill: var(--sunken, #eef3ef); stroke: var(--heading, #17232d); }
.arch__node--store rect { fill: var(--info-bg, #e8f0f7); stroke: var(--info-fg, #2c5f8a); }
.arch__node--job rect { fill: var(--warn-bg, #fbf3dc); stroke: var(--warn-fg, #8a6b12); }
.arch__node--seam rect { stroke-dasharray: 5 4; }
.arch__node--outside rect { fill: var(--bg-soft, #f3f6f4); }
.arch__label { font-size: 14px; font-weight: 600; fill: var(--heading, #17232d); }
.arch__detail { font-size: 11.5px; fill: var(--muted, #5f6d7a); }
.arch__edge-label { font-size: 11px; fill: var(--muted, #5f6d7a); }

/* --- Settings by theme (1.2) ------------------------------------------------ */
.settings-group { margin: var(--sp-7, 2.5rem) 0 var(--sp-3); font-size: .72rem; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.settings-index { display: flex; flex-wrap: wrap; gap: .4rem 1.1rem; margin: 0 0 var(--sp-3); font-size: .9rem; }
.settings-index a { text-decoration: none; font-weight: 600; }
.settings-index a:hover { text-decoration: underline; }
/* Tabs of a group of pages (callbacks, and every group of the operator's menu): one look everywhere. */
.home-extras { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); gap: var(--sp-4); }
.callback-tabs, .page-tabs { display: flex; flex-wrap: wrap; gap: .4rem 1.6rem; margin: 0 0 var(--sp-4); font-size: .9rem;
  border-bottom: 1px solid var(--border); }
.callback-tabs a, .page-tabs a { text-decoration: none; font-weight: 600; padding: .45rem 0 .5rem; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.callback-tabs a:hover, .page-tabs a:hover { text-decoration: none; color: var(--heading); }
.callback-tabs a[aria-current="page"], .page-tabs a[aria-current="page"] { color: var(--heading); border-bottom-color: var(--primary); }
/* The operator's pages write to the page's width: a line of text is not held to half of it (owner 2026-10-27).
   Inputs keep their own widths; only running text, notes and hints are freed. */
.app--admin .console-main p, .app--admin .console-main .muted, .app--admin .console-main .micro { max-width: none !important; }
/* The operator's head: the title on the left, the page's action all the way right on its line, the
   subtitle below at full width (Users' shape, every page). */
.app--admin .page-head { align-items: center; margin-bottom: var(--sp-3); }
.app--admin .page-head h1 { margin: 0; }
.app--admin .page-head__actions .button-link, .app--admin .page-head__actions button { white-space: nowrap; }
/* One button, three colours (owner 2026-10-27): the same shape everywhere, only the colour changes: the plain button
   (green), `secondary` and `danger`. `.btn`, `.btn--secondary` and `.btn--ghost` are the older names; a link that is an
   action is `a.button-link`, with the same two modifiers. */
button.btn--secondary, button.btn--ghost, a.button-link.secondary, a.btn--secondary, a.btn--ghost {
  background: transparent; color: var(--body); border: 1px solid var(--border-strong); box-shadow: none;
}
button.btn--secondary:hover, button.btn--ghost:hover, a.button-link.secondary:hover, a.btn--secondary:hover, a.btn--ghost:hover {
  background: var(--sunken); color: var(--heading);
}
a.btn { display: inline-block; text-decoration: none; padding: .58em 1.25em; border-radius: var(--radius-sm); font-size: .86rem; }
a.button-link.danger { background: var(--danger); box-shadow: 0 2px 4px rgba(201, 58, 31, .35); }
a.button-link.danger:hover { background: #a82e17; }
/* A form's actions sit together at the right, the success action first and the others after it, the least
   important last (Save, then Cancel). */
.form-actions { align-items: center; }
.org-picker__row { display: flex; gap: .5rem; align-items: center; }
.org-picker__row input { flex: 1 1 auto; min-width: 0; }
.org-picker__box { margin-top: .4rem; padding: .6rem; border: 1px solid var(--border); border-radius: var(--radius-sm, 6px); background: var(--surface); max-width: 34rem; }
.org-picker__box input { width: 100%; box-sizing: border-box; }
.org-picker__results { list-style: none; margin: .5rem 0 0; padding: 0; max-height: 16rem; overflow: auto; }
.org-picker__row-button { display: block; width: 100%; text-align: left; background: none; border: 0; padding: .4rem .5rem; cursor: pointer; font: inherit; color: inherit; border-radius: 4px; }
.org-picker__row-button:hover, .org-picker__row-button:focus-visible { background: var(--sunken); }
.page-tabs__n { display: inline-block; min-width: 1.3em; padding: 0 .4em; margin-left: .25em; border-radius: 1em; text-align: center;
  background: var(--warn-bg); color: var(--warn-fg); font-size: .75rem; font-weight: 700; }

/* A box that opens a form (1.2): the text of the claim appears once the box is ticked; with no
   :has() support the form simply shows (the browser ignores the rule). */
.reveal-form .reveal-body { display: none; margin-top: var(--sp-3); }
.reveal-form:has(.reveal-toggle:checked) .reveal-body { display: block; }

/* ==========================================================================
   Phones and touch (1.2, 2026-10-21)
   ==========================================================================
   Owner: *tutto desktop ottimizzato e mobile friendly*. Measured by
   test/e2e/test_templates_responsive.py at 1440, 1280, 768 (touch), 390 and 320 (touch):
   on a phone or any coarse pointer the primary actions and the navigation are at least
   44 x 44 CSS px (WCAG 2.5.5), every form control is written at 16 px (under 16 px iOS
   Safari zooms the page into a focused field and leaves it zoomed, wider than the phone),
   and a table scrolls in its own box. A mouse on a desktop sees none of this: every rule
   is inside the media query. `#main` gives these rules the weight to pass a page's own
   sizes (`.tree__inline input`, `.studio__prompt`) without `!important`. */

/* A table inside a panel scrolls in its own box rather than widening the page (a
   `.table-wrap` without the card around it, for a table that is already in one). */
.table-scroll { overflow-x: auto; max-width: 100%; }
.table-scroll:has(details[open]) { overflow: visible; }

@media (max-width: 640px), (pointer: coarse) {
  #main button, #main input[type=submit], #main a.button-link, #main .quick a,
  .rail .rail__item {
    min-height: 44px; min-width: 44px; box-sizing: border-box;
  }
  #main button, #main input[type=submit] { touch-action: manipulation; }
  /* 16px on every touch screen, a tablet too (iOS zooms into a smaller field; run4, 10-27:
     the Branding fields were 12.75-14.4px on an iPad Mini). */
  #main input, #main select, #main textarea { font-size: 16px; }
  #main a.button-link { display: inline-flex; align-items: center; }
  /* A sortable header is a button too, drawn as the header: it keeps its own height. */
  #main th.sortable-th > button { min-height: 0; min-width: 0; }

  #main input:not([type=checkbox]):not([type=radio]):not([type=hidden]), #main select {
    min-height: 44px; box-sizing: border-box;
  }
  #main input[type=file] { padding-block: .55em; }
  /* A box is tapped through the words beside it: the label is the target. */
  #main label:has(> input[type=checkbox]), #main label:has(> input[type=radio]) {
    min-height: 44px; align-items: center;
  }

  #main details > summary { min-height: 44px; box-sizing: border-box; }
  /* The ones drawn as a line of text get the room as padding (the deeper / fold / org
     headers already have more of their own). */
  #main details:not(.deeper):not(.fold):not(.org) > summary { padding-block: .65em; }
  #main .kebab > summary {
    width: 2.75rem; height: 2.75rem; min-height: 0; line-height: 2.75rem; padding: 0;
  }
  #main .kebab__menu { top: calc(100% + .2em); }
  #main .kebab__item { min-height: 44px; }
  #main .tree__edit > summary { display: inline-flex; align-items: center; padding-block: 0; }
  #main .tree__template a { min-height: 44px; display: inline-flex; align-items: center; }
  #main .snips__tabs label { min-height: 44px; display: inline-flex; align-items: center; }
  #main .icon-action { width: 2.75rem; height: 2.75rem; }
}

@media (max-width: 640px) {
  #main input, #main select, #main textarea { font-size: 16px; }
  .rail .guide-search input { font-size: 16px; }
  .card, .panel { padding: var(--sp-4); }
  /* The folder controls: a field and its button, wrapping under each other when the
     branch is too deep for both on one line. */
  #main .tree__inline { flex-wrap: wrap; }
  /* A head's actions may shrink and wrap on a phone: Home's Period and Buy credit side by side
     made the page 332 px wide at 320 (rc13 e2e, 10-30), the actions being `flex: 0 0 auto`. */
  .page-head__actions, .t-head__actions { flex-shrink: 1; min-width: 0; max-width: 100%; }
  .t-head__actions > form { flex-wrap: wrap; min-width: 0; max-width: 100%; }
  .t-head__actions select { min-width: 0; max-width: 100%; }
  #main .tree__inline input, #main .tree__inline select { flex: 1 1 9rem; min-width: 0; width: auto; }
  /* Deep in the tree on a 320 px phone the fields' own width still ran 48-68 px past the
     screen (rc13 e2e, 10-30): every box on the way may shrink, and a field asks for no width
     of its own, so it takes the line and its button wraps under it. */
  #main .tree__tools > details, #main .tree__actions, #main .tree__inline { min-width: 0; max-width: 100%; }
  #main .tree__tools > details { flex: 1 1 100%; }
  #main .tree__inline { width: 100%; }
  #main .tree__inline input, #main .tree__inline select { width: 0; flex-basis: 9rem; }
  /* A table a template forgot to wrap still scrolls in its own box, never the page. */
  .panel > table, .deeper__body > table, .fold__body > table, .sub > table {
    display: block; overflow-x: auto; max-width: 100%;
  }
  .rail .guide-search__results { min-width: 0; max-width: calc(100vw - 2rem); }
}

/* The success action sits at the far right, Cancel at the left (owner 2026-10-27): the markup keeps
   the success action first, the row is drawn reversed. */
.field-row.field-row--load { align-items: end; }
.charts-pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-4); }
@media (max-width: 900px) { .charts-pair { grid-template-columns: 1fr; } }
.charts-pair .panel { margin-bottom: 0; min-width: 0; }
dialog.dialog { max-width: min(42rem, 92vw); width: 100%; border: 0; border-radius: var(--radius-sm, 6px); padding: var(--sp-5);
  box-shadow: 0 12px 40px rgba(0,0,0,.25); }
dialog.dialog::backdrop { background: rgba(0,0,0,.45); }

/* Quick actions: every button the same width, the row divided in equal parts (owner 2026-10-27). */
.quick--equal { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: var(--sp-3); }
.quick--equal a { justify-content: center; text-align: center; }
@media (max-width: 720px) { .quick--equal { grid-auto-flow: row; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-columns: auto; } }

/* The list pages' filters in one row, smaller (owner 2026-10-27): groups side by side, the count at the end. */
.filter-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-5); margin-bottom: var(--sp-4); }
.filter-row > nav.quick { gap: var(--sp-1); margin: 0; flex-wrap: nowrap; }
.filter-row > nav.quick:has([data-kind-totals]) { flex: 1 1 auto; }
.filter-row .quick a { padding: .28em .75em; font-size: .78rem; min-height: 0; }
.filter-row [data-kind-totals] { margin-left: auto; font-size: .8rem; }
@media (max-width: 720px) { .filter-row > nav.quick { flex-wrap: wrap; } }
/* A yellow exclamation where a document left fields empty; the words are the tooltip. */
.gap-col { width: 2.2rem; text-align: center; }
.gap-flag { display: inline-flex; align-items: center; justify-content: center; width: 1.35rem; height: 1.35rem; border-radius: 50%;
  background: #f5c518; color: #3d2f00; font-weight: 700; font-size: .85rem; cursor: help; }

/* Getting started on the Home (1.3, 2026-11-01): numbered steps, a done one turns green. */
.start__steps { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: grid; gap: var(--sp-3); }
.start__step { display: flex; gap: var(--sp-3); align-items: flex-start; padding: var(--sp-3); border: 1px solid var(--border); border-radius: 8px; }
.start__step p { margin: 4px 0 8px; }
.start__step.is-done { opacity: .75; }
.start__mark { flex: none; width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center; font-weight: 600; border: 1px solid var(--border-strong); }
.start__step.is-done .start__mark { background: var(--ok-bg); color: var(--ok-fg); border-color: transparent; }
