﻿/* ==================================================================
   POSTO portal — shell

   Every colour, radius, shadow, type step and duration comes from
   tokens.css. Nothing is redefined here: this file describes shell
   structure only, so a token change lands on the shell and all four
   modules at once.
   ================================================================== */

/* tokens.css is linked ahead of this file in the document head — a <link>
   parallelises where an @import would force a serial round-trip. */

:root {
  --topbar-h: 58px;
  --sidenav-w: 244px;
  --measure: 68ch; /* ~65 characters — the readable line length */
  --focus: 0 0 0 2px var(--bg), 0 0 0 3px var(--brand-2);
}

* { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  font-family: var(--sans);
  font-size: var(--step-0);
  color: var(--text);
  background:
    radial-gradient(1100px 620px at 82% -8%, #16234a 0%, transparent 58%),
    radial-gradient(900px 520px at 6% 2%, #122043 0%, transparent 55%),
    var(--bg);
  background-attachment: fixed;
  line-height: 1.6;
  min-height: 100dvh; /* dvh, not vh — mobile URL bar */
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
}

/* Four weights, not two — 500 and 600 carry most of the hierarchy so
   headings don't have to shout at 700 to read as headings. */
h1, h2, h3, h4 {
  margin: 0;
  line-height: 1.18;
  text-wrap: balance; /* no orphaned last words in headings */
}
h1 { font-size: var(--step-4); font-weight: 600; letter-spacing: var(--track-display); }
h2 { font-size: var(--step-2); font-weight: 600; letter-spacing: var(--track-title); }
h3 { font-size: var(--step-1); font-weight: 550; letter-spacing: var(--track-title); }
h4 { font-size: var(--step-0); font-weight: 600; }
p { margin: 0; text-wrap: pretty; }
ul { margin: 0; padding: 0; list-style: none; }
a { color: var(--brand-2); }

code, pre, .font-mono { font-family: var(--mono); }
/* Tabular figures keep credit counts and IP columns from jittering. */
.tnum, .meter__value, td.num { font-variant-numeric: tabular-nums; }

.icon { flex: none; }

/* Visually hidden but announced — for labels on compact inline controls. */
.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;
}

/* --- focus: never removed, always visible ------------------------- */
:focus-visible {
  outline: none;
  box-shadow: var(--focus);
  border-radius: var(--radius-sm);
}

.skip-link {
  position: absolute;
  left: var(--space-3);
  top: -100px;
  z-index: var(--z-toast);
  padding: var(--space-3) var(--space-4);
  background: var(--brand);
  color: var(--brand-ink);
  font-weight: 650;
  border-radius: var(--radius-sm);
  transition: top 0.15s var(--ease-out);
}
.skip-link:focus { top: var(--space-3); }

/* ==================================================================
   App frame
   ================================================================== */

.app {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: var(--topbar-h) 1fr;
  grid-template-areas: 'topbar' 'main';
  min-height: 100dvh;
}

@media (min-width: 1024px) {
  .app {
    grid-template-columns: var(--sidenav-w) 1fr;
    grid-template-areas:
      'topbar topbar'
      'sidenav main';
  }
}

/* ---------------- Topbar ---------------- */

.topbar {
  grid-area: topbar;
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  height: var(--topbar-h);
  padding-inline: var(--space-3);
  /* Safe-area inset for notched devices in landscape. */
  padding-left: max(var(--space-3), env(safe-area-inset-left));
  padding-right: max(var(--space-3), env(safe-area-inset-right));
  background: var(--bar-bg);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border-soft);
}

.topbar__spacer { flex: 1; }

.topbar__burger {
  display: inline-grid;
  place-items: center;
  width: 44px;  /* touch-target-size */
  height: 44px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
}
@media (min-width: 1024px) { .topbar__burger { display: none; } }

.brand {
  display: inline-flex;
  align-items: center;
  flex: none;
  text-decoration: none;
  border-radius: var(--radius-sm);
}
/* The wordmark carries its own colour, so it is an <img> rather than an inline
   SVG — nothing here should try to recolour it. Height is fixed and width
   derived, so the 5.19:1 artwork never distorts. */
.brand__logo {
  display: block;
  height: 25px;
  width: auto;
  max-width: 46vw;
}
.brand:focus-visible { box-shadow: var(--focus); }

.brand--auth {
  justify-content: center;
  margin-bottom: var(--space-5);
}
.brand--auth .brand__logo { height: 38px; }

@media (max-width: 480px) {
  .brand__logo { height: 21px; }
}

.account {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.account__email {
  font-size: 0.85rem;
  color: var(--text-dim);
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
@media (max-width: 719px) { .account__email { display: none; } }

.badge {
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 3px var(--space-2);
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--text-dim);
  background: var(--surface-2);
}
.badge--admin { color: var(--brand-2); border-color: var(--brand-line); background: var(--brand-dim); }
.badge--active { color: var(--green); border-color: var(--green-line); background: var(--green-soft); }
.badge--pending { color: var(--yellow); border-color: var(--yellow-line); background: var(--yellow-soft); }
.badge--suspended { color: var(--red); border-color: var(--red-line); background: var(--red-soft); }

/* ---------------- Credit meter ---------------- */

/* Admin variant: one chip, no bar, no note. */
.meter-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px var(--space-3);
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-dim);
  font-size: var(--step--1);
  font-weight: 550;
  white-space: nowrap;
}
.meter-chip .icon { color: var(--green); }

/* Hairline between the balance and the account block — they are different
   kinds of information and were reading as one run-on cluster. */
.topbar__rule {
  width: 1px;
  height: 22px;
  flex: none;
  background: var(--border-soft);
}
@media (max-width: 899px) { .topbar__rule { display: none; } }

.meter { min-width: 168px; }
.meter__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}
.meter__label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-faint);
  font-weight: 650;
}
.meter__value { font-size: 0.82rem; color: var(--text-dim); }
.meter__value strong { color: var(--text); font-size: 0.95rem; }

.meter__track {
  height: 6px;
  margin-top: var(--space-1);
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
}
.meter__fill {
  height: 100%;
  border-radius: inherit;
  background: var(--brand);
  transition: width 0.3s var(--ease-out);
}
.meter--low .meter__fill { background: var(--yellow); }
.meter--low .meter__value strong { color: var(--yellow); }
.meter--empty .meter__fill { background: var(--red); }
.meter--empty .meter__value strong { color: var(--red); }

.meter__note {
  margin-top: var(--space-1);
  font-size: 0.72rem;
  color: var(--text-faint);
}

.topbar__meter { display: none; }
@media (min-width: 900px) { .topbar__meter { display: flex; align-items: center; } }

/* Three stacked rows don't fit a 58px bar. The reset date is still shown in
   full on Overview and Settings, so the topbar keeps only the balance. */
.topbar__meter .meter__note { display: none; }
.topbar__meter .meter { min-width: 150px; }
.topbar__meter .meter__head { gap: var(--space-4); }

/* ---------------- Side navigation ---------------- */

.sidenav {
  grid-area: sidenav;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-3);
  padding-bottom: max(var(--space-4), env(safe-area-inset-bottom));
  border-right: 1px solid var(--border-soft);
  background: var(--rail-bg);
  overflow-y: auto;
}

/*
 * The sidebar stays put while the page scrolls.
 *
 * It is a grid item, so by default it is only as tall as its row and travels
 * up with the document — on a long page (the users table, a generated report
 * card) the navigation scrolls out of reach and getting back to another tool
 * means scrolling to the top first.
 *
 * `align-self: start` is what makes sticky work here: without it the item
 * stretches to fill the grid row, and an element already as tall as its
 * containing block has nothing to stick against.
 *
 * The height is the viewport minus the topbar it sits under, in dvh rather
 * than vh so a mobile browser's collapsing URL bar does not leave the last nav
 * item under the chrome. `overflow-y: auto` is already set above, so a nav
 * taller than the viewport scrolls within itself rather than being cut off.
 */
@media (min-width: 1024px) {
  .sidenav {
    position: sticky;
    top: var(--topbar-h);
    align-self: start;
    height: calc(100dvh - var(--topbar-h));
  }

  /*
   * Reclaim the chrome, not the targets.
   *
   * An admin sees 14 nav items, and on an 820px-tall screen that overflowed the
   * sticky column by 47px — putting Sign out below the fold inside a nav that
   * is itself pinned. The 47px comes back out of the container's own padding
   * and the gap above the footer; the 44px item height stays, because that is
   * the minimum comfortable pointer target and shaving it to fit one more row
   * is how a nav ends up feeling cheap.
   */
  .sidenav {
    gap: var(--space-2);
    padding-block: var(--space-2);
    padding-bottom: max(var(--space-2), env(safe-area-inset-bottom));
  }
  .sidenav__foot { gap: var(--space-2); }
  .sidenav__foot .signout { padding-top: var(--space-2); }
}

/* Below 1024 the sidenav becomes an off-canvas drawer. */
@media (max-width: 1023px) {
  .sidenav {
    position: fixed;
    inset: var(--topbar-h) auto 0 0;
    width: min(300px, 84vw);
    z-index: var(--z-drawer);
    background: var(--bg-2);
    transform: translateX(-102%);
    transition: transform 0.24s var(--ease-out);
    box-shadow: var(--shadow);
  }
  .sidenav[data-open='true'] { transform: translateX(0); }
}

.sidenav__close { display: none; }
@media (max-width: 1023px) {
  .sidenav__close {
    display: inline-grid;
    place-items: center;
    align-self: flex-end;
    width: 44px;
    height: 44px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
    cursor: pointer;
  }
}

.drawer-scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-scrim);
  /* 55% — strong enough to isolate the drawer, per scrim legibility. */
  background: var(--scrim);
  backdrop-filter: blur(2px);
}
.drawer-scrim[hidden] { display: none; }

/*
 * `hidden` must actually hide.
 *
 * The attribute's UA rule is `[hidden] { display: none }` — specificity 0,1,0,
 * exactly the same as any class that sets display. So `.btn { display: flex }`,
 * loaded later, silently wins, and an element marked hidden stays on screen.
 * That is how the sync panel's Stop button came to be permanently visible with
 * no run in progress, inviting a click that does nothing.
 *
 * It had been patched element by element (.drawer-scrim above, and nine more in
 * dashboard.css) which treats the symptom and guarantees the next element to
 * need it will be forgotten. This is the one place !important is the right
 * tool: the attribute is a statement of intent, and no layout rule — in this
 * file or a later one — should be able to override it.
 */
[hidden] { display: none !important; }
@media (min-width: 1024px) { .drawer-scrim { display: none !important; } }

.sidenav__list { display: flex; flex-direction: column; gap: 2px; }
.sidenav__foot { margin-top: auto; display: flex; flex-direction: column; gap: var(--space-3); }
.sidenav__foot .signout {
  padding-top: var(--space-3);
  border-top: 1px solid var(--border-soft);
}

.navitem {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 44px; /* touch-target-size */
  padding: var(--space-2) var(--space-3);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text-dim);
  background: none;
  font: inherit;
  font-size: 0.9rem;
  font-weight: 550;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  position: relative;
  transition: background 0.15s var(--ease-out), color 0.15s var(--ease-out);
}
.navitem:hover { background: var(--surface); color: var(--text); }

/* Active state uses an accent rail + weight + colour — three cues, not one. */
.navitem[aria-current='page'] {
  background: var(--surface-2);
  color: var(--text);
  font-weight: 650;
  border-color: var(--border);
}
.navitem[aria-current='page']::before {
  content: '';
  position: absolute;
  left: -1px;
  top: 50%;
  transform: translateY(-50%);
  width: 3px;
  height: 22px;
  border-radius: 0 3px 3px 0;
  background: var(--brand);
}
.navitem[aria-current='page'] .icon { color: var(--brand-2); }

.navitem__label { flex: 1; min-width: 0; }

.navitem__cost {
  flex: none;
  font-size: 0.68rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 1px 7px;
}
.navitem--danger { color: var(--text-faint); }
.navitem--danger:hover { background: var(--red-soft); color: var(--red); }

/* ---------------- Main ---------------- */

.main {
  grid-area: main;
  min-width: 0; /* stops wide tables forcing body-level horizontal scroll */
  /* The content surface has to reach the bottom of the viewport even when the
     page is short. Without this the light area stops at the last card and the
     dark body shows beneath it, which reads as a rendering fault rather than
     as a short page. */
  min-height: 100%;
  background: var(--bg);
  padding: var(--space-5) var(--space-4) var(--space-7);
  padding-left: max(var(--space-4), env(safe-area-inset-left));
  padding-right: max(var(--space-4), env(safe-area-inset-right));
}
.main:focus { outline: none; }

.wrap { max-width: var(--page-max); margin-inline: auto; }

/* ==================================================================
   Page anatomy

   Every shell page is Page > PageHeader > Section*. Spacing and
   heading treatment live here once, so no page can quietly invent its
   own rhythm.
   ================================================================== */

.page { max-width: var(--page-max); margin-inline: auto; }
.page--narrow { max-width: 760px; }

/* Sections are separated by a consistent step, and by a hairline rule
   rather than by each one being a floating card. */
.page > .section + .section {
  margin-top: var(--space-7);
  padding-top: var(--space-6);
  border-top: 1px solid var(--border-soft);
}

.pagehead { margin-bottom: var(--space-6); }
.pagehead__row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
}
.pagehead__text { min-width: 0; }
.pagehead__eyebrow {
  font-size: var(--step--1);
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  color: var(--text-faint);
  margin-bottom: var(--space-2);
}
.pagehead__lede {
  margin-top: var(--space-3);
  color: var(--text-dim);
  font-size: var(--step-0);
  max-width: var(--measure);
}
.pagehead__actions { flex: none; }

/* Headline figure in the page header — the balance, without the meter. */
.headstat {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  background: var(--surface);
}
.headstat__value {
  font-size: var(--step-3);
  font-weight: 650;
  letter-spacing: var(--track-display);
  line-height: 1;
}
.headstat__label { font-size: var(--step--1); color: var(--text-faint); line-height: 1.35; }

/* ---------------- Section ---------------- */

.section__head { margin-bottom: var(--space-4); }
.section__title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: var(--track-title);
}
.section__title .icon { color: var(--text-faint); }
.section__desc {
  margin-top: var(--space-2);
  color: var(--text-dim);
  font-size: var(--step--1);
  max-width: var(--measure);
}

.section--boxed {
  padding: var(--space-5);
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
}
/* A boxed section is already visually separated — don't rule it as well. */
.page > .section--boxed + .section--boxed {
  margin-top: var(--space-4);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border-soft);
}

.stack { display: flex; flex-direction: column; gap: var(--space-4); }

/* ---------------- Split: main + rail ---------------- */

.split { display: grid; gap: var(--space-5); grid-template-columns: 1fr; }
@media (min-width: 960px) {
  .split { grid-template-columns: minmax(0, 1fr) 300px; align-items: start; }
  .split__rail { position: sticky; top: calc(var(--topbar-h) + var(--space-5)); }
}
.split__main { display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; }

.rail {
  padding: var(--space-5);
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
}
.rail__title {
  font-size: var(--step--1);
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  color: var(--text-faint);
  margin-bottom: var(--space-4);
}
.rail__list { display: flex; flex-direction: column; gap: var(--space-3); margin: 0; }

.datarow {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}
.datarow + .datarow { padding-top: var(--space-3); border-top: 1px solid var(--border-soft); }
.datarow__label { font-size: var(--step--1); color: var(--text-faint); flex: none; }
.datarow__value {
  margin: 0;
  font-size: var(--step--1);
  color: var(--text);
  text-align: right;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* ---------------- Stat strip ---------------- */

.statstrip {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  margin-bottom: var(--space-5);
}
.stat {
  padding: var(--space-4) var(--space-5);
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  /* Accent rail carries the tone; the number stays legible either way. */
  border-left: 3px solid var(--border);
}
.stat--green { border-left-color: var(--green); }
.stat--yellow { border-left-color: var(--yellow); }
.stat--red { border-left-color: var(--red); }
.stat__value {
  font-size: var(--step-3);
  font-weight: 650;
  letter-spacing: var(--track-display);
  line-height: 1.1;
}
.stat__label {
  margin-top: var(--space-1);
  font-size: var(--step--1);
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
}

/* ---------------- Disclosure ---------------- */

.disclosure {
  margin-bottom: var(--space-6);
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  overflow: hidden;
}
.disclosure__summary {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 52px;
  padding: var(--space-3) var(--space-5);
  cursor: pointer;
  font-weight: 600;
  font-size: var(--step-0);
  list-style: none;
  transition: background var(--dur) var(--ease-out);
}
.disclosure__summary::-webkit-details-marker { display: none; }
.disclosure__summary:hover { background: var(--surface-2); }
.disclosure__summary .icon { color: var(--text-faint); }
.disclosure__summary > span { flex: 1; }
.disclosure__chev { transition: transform var(--dur) var(--ease-out); }
.disclosure[open] .disclosure__chev { transform: rotate(90deg); }
.disclosure__body {
  padding: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border-soft);
}

.emptystate--boxed {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
}

/* ==================================================================
   Components
   ================================================================== */

.card {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  padding: var(--space-5);
  box-shadow: var(--shadow);
}
.card__title { display: flex; align-items: center; gap: var(--space-2); }
.card__body { margin-top: var(--space-4); display: flex; flex-direction: column; gap: var(--space-4); }

/* ---------------- Tool launcher ---------------- */

/* An asymmetric grid, not N equal columns. The first tool spans two
   tracks and carries more weight; the rest fall into a denser rhythm
   beside it. Deliberately not a row of identical thirds. */
.launcher {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: 1fr;
}
@media (min-width: 720px) {
  .launcher { grid-template-columns: repeat(2, 1fr); }
  .launcher > .toolcard:first-child { grid-column: span 2; }
}
@media (min-width: 1180px) {
  .launcher { grid-template-columns: repeat(3, 1fr); }
  /* Two columns wide, one row tall. Spanning two rows as well left a large
     dead area inside the card that no amount of padding fixed. */
  .launcher > .toolcard:first-child { grid-column: span 2; }
}

/* No border + shadow + background all at once. The card is a surface
   and spacing; a hairline appears only on hover, where it means
   something. */
.toolcard {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-5);
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: var(--surface);
  color: inherit;
  text-decoration: none;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  transition: transform var(--dur) var(--ease-spring),
    border-color var(--dur) var(--ease-out), background var(--dur) var(--ease-out);
}
.toolcard:hover {
  transform: translateY(-3px);
  background: var(--surface-2);
  border-color: var(--border);
}
.toolcard:active { transform: translateY(-1px); }

/* Accent hairline along the top edge, drawn on hover only. */
.toolcard::before {
  content: '';
  position: absolute;
  inset: 0 auto auto 0;
  width: 100%;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--brand-line), transparent);
  opacity: 0;
  transition: opacity var(--dur) var(--ease-out);
}
.toolcard:hover::before { opacity: 1; }

.toolcard__top { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); }
.toolcard__icon {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: var(--radius-sm);
  background: var(--surface-3);
  color: var(--text-dim);
  transition: color var(--dur) var(--ease-out), background var(--dur) var(--ease-out);
}
.toolcard:hover .toolcard__icon { color: var(--brand-2); background: var(--brand-dim); }

.toolcard__name {
  font-size: var(--step-1);
  font-weight: 550;
  letter-spacing: var(--track-title);
}
.toolcard__blurb {
  color: var(--text-dim);
  font-size: var(--step--1);
  max-width: 46ch;
  margin-top: var(--space-1);
}

/* Pinned to the bottom so the "Open" affordances line up across the
   row no matter how long each blurb runs. */
.toolcard__foot {
  margin-top: auto;
  padding-top: var(--space-4);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  font-size: var(--step--1);
  color: var(--text-faint);
}
.toolcard__go {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--text-dim);
  font-weight: 550;
  transition: color var(--dur) var(--ease-out), gap var(--dur) var(--ease-out);
}
.toolcard:hover .toolcard__go { color: var(--brand-2); gap: 10px; }

/* The lead card gets room to breathe rather than a louder colour. */
@media (min-width: 1180px) {
  .launcher > .toolcard:first-child { padding: var(--space-6); }
  .launcher > .toolcard:first-child .toolcard__name { font-size: var(--step-2); }
  .launcher > .toolcard:first-child .toolcard__icon { width: 46px; height: 46px; }
}

.cost-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px var(--space-2);
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--text-dim);
}
.cost-chip--free { color: var(--green); border-color: var(--green-line); background: var(--green-soft); }

/* Disabled tool card: allowance spent. Not a link, and says why. */
.toolcard--locked { opacity: 0.55; pointer-events: none; }
.toolcard--locked .toolcard__go { color: var(--text-faint); }

/* ---------------- Buttons (API matches the modules' existing .btn) --- */

.btn {
  --btn-bg: var(--surface-2);
  --btn-fg: var(--text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  font: inherit;
  font-weight: 600;
  font-size: 0.92rem;
  min-height: 44px;
  padding: 0.62rem 1.1rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--btn-bg);
  color: var(--btn-fg);
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  transition: transform 0.12s var(--ease-out), background 0.15s var(--ease-out),
    border-color 0.15s var(--ease-out), box-shadow 0.15s var(--ease-out);
}
.btn:hover { transform: translateY(-1px); border-color: var(--hover-line); }
.btn:active { transform: translateY(0); }
.btn:disabled, .btn[aria-disabled='true'] {
  opacity: 0.45; /* disabled-states: 0.38–0.5 */
  cursor: not-allowed;
  transform: none;
}
.btn--primary {
  --btn-fg: var(--brand-ink);
  background: var(--brand-grad);
  border-color: transparent;
  box-shadow: var(--shadow-accent);
}
.btn--primary:hover {
  border-color: transparent;
  box-shadow: var(--shadow-accent);
}
.btn--ghost { background: transparent; }
.btn--small { min-height: 36px; padding: 0.42rem 0.8rem; font-size: 0.84rem; }
.btn--danger { --btn-fg: var(--red); border-color: var(--red-line); background: var(--red-soft); }
.btn--block { width: 100%; }

.btn__spinner {
  width: 15px;
  height: 15px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.btn[data-loading='true'] { pointer-events: none; opacity: 0.7; }

/* ---------------- Forms ---------------- */

.field { display: flex; flex-direction: column; gap: var(--space-2); }
.field + .field { margin-top: var(--space-4); }
/* ...but not inside a grid, where `gap` already spaces them.

   The adjacent-sibling rule above is for fields stacked in a column. In
   .field-grid it applied as well, so every field except the first carried an
   extra 16px of top margin and sat lower than its neighbours: the first label
   in every multi-column row lined up one step above the rest. Visible on the
   provision form (Email / Monthly credits / Role) and on the dispatch form
   (Spamhaus threat type / Abusix category / Abusix type). */
.field-grid > .field + .field { margin-top: 0; }

.field__label {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text-dim);
}
.field__label .req { color: var(--red); margin-left: 2px; }

.input, .select, .textarea {
  font: inherit;
  font-size: 1rem; /* 16px stops iOS zoom-on-focus */
  min-height: 44px;
  width: 100%;
  padding: 0.6rem 0.8rem;
  color: var(--text);
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  transition: border-color 0.15s var(--ease-out);
}
.input::placeholder { color: var(--text-faint); }
.input:hover, .select:hover, .textarea:hover { border-color: var(--hover-line); }
.input[aria-invalid='true'] { border-color: var(--red); }
.input:read-only { background: var(--surface); color: var(--text-dim); }
.input:disabled { opacity: 0.45; cursor: not-allowed; }

/* The demo form's honeypot field.

   Deliberately NOT .sr-only, which is the opposite of what this needs: that
   hides from sight but keeps a field announced to screen readers, and a
   honeypot a screen-reader user can fill in is a trap for the wrong person.
   This hides it from everyone, and the markup carries aria-hidden and
   tabindex="-1" so neither assistive tech nor the tab key ever reaches it.

   Off-screen rather than display:none because a bot that parses CSS can skip
   a field that is not displayed; moving it costs the same and reads as a real
   input to anything that is not rendering the page. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.field__help { font-size: 0.78rem; color: var(--text-faint); }
/* Errors sit below the field they belong to, never only at the top. */
.field__error {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  font-size: 0.8rem;
  color: var(--red);
  font-weight: 550;
}

.input-group { position: relative; display: flex; }
.input-group .input { padding-right: 48px; }
.input-group__btn {
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border: 0;
  background: none;
  color: var(--text-faint);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.input-group__btn:hover { color: var(--text); }

/* One-time-code input: wide tracking, monospace, tabular. */
.otp {
  font-family: var(--mono);
  font-size: 1.5rem;
  letter-spacing: 0.5em;
  text-align: center;
  text-indent: 0.5em;
  font-variant-numeric: tabular-nums;
}

/* ---------------- Alerts ---------------- */

.alert {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  font-size: 0.88rem;
  color: var(--text-dim);
}
.alert .icon { margin-top: 2px; flex: none; }
.alert strong { color: var(--text); display: block; }
.alert--error { border-color: var(--red-line); background: var(--red-soft); color: var(--red-ink); }
.alert--error .icon { color: var(--red); }
.alert--success { border-color: var(--green-line); background: var(--green-soft); color: var(--green-ink); }
.alert--success .icon { color: var(--green); }
.alert--warn { border-color: var(--yellow-line); background: var(--yellow-soft); color: var(--yellow-ink); }
.alert--warn .icon { color: var(--yellow); }

/* ---------------- Tables ---------------- */

/* Wide tables scroll inside their own box; the page body never scrolls sideways. */
.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  background: var(--surface);
}
.table { width: 100%; border-collapse: collapse; font-size: 0.88rem; min-width: 720px; }
.table th, .table td {
  padding: var(--space-3) var(--space-4);
  text-align: left;
  border-bottom: 1px solid var(--border-soft);
  vertical-align: middle;
}
.table thead th {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  /* --text-dim, not --text-faint: faint on the --bg-2 header row measured
     4.49:1, just under AA for 11.5px text. */
  color: var(--text-dim);
  font-weight: 700;
  background: var(--bg-2);
  position: sticky;
  top: 0;
}
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr:hover { background: var(--row-hover); }
/* Header and cells together. Only td.num was aligned, so a numeric column's
   heading sat at its left edge while the figures sat at its right - in the
   Postmaster tables "Mean" read as the heading of the Latest column's value. */
.table th.num,
.table td.num { text-align: right; }

.emptystate {
  padding: var(--space-7) var(--space-5);
  text-align: center;
  color: var(--text-dim);
}
.emptystate .icon { color: var(--text-faint); margin-bottom: var(--space-3); }
.emptystate h3 { margin-bottom: var(--space-2); color: var(--text); }

/* ---------------- Auth ---------------- */

.authbody { display: grid; place-items: center; min-height: 100dvh; padding: var(--space-4); }
.auth { width: 100%; max-width: 420px; }
.auth__card {
  padding: var(--space-6) var(--space-5);
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.auth__foot { margin-top: var(--space-4); text-align: center; font-size: 0.8rem; color: var(--text-faint); }
.auth h1 { font-size: 1.25rem; margin-bottom: var(--space-2); }
.auth__lede { color: var(--text-dim); font-size: 0.9rem; margin-bottom: var(--space-5); }

.divider {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-5) 0;
  color: var(--text-faint);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.divider::before, .divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border-soft);
}

.qr-panel {
  display: grid;
  place-items: center;
  gap: var(--space-3);
  padding: var(--space-4);
  background: #fff; /* QR needs a light quiet zone to scan reliably */
  border-radius: var(--radius-sm);
}
.secret-readout {
  font-family: var(--mono);
  font-size: 0.85rem;
  letter-spacing: 0.08em;
  word-break: break-all;
  padding: var(--space-3);
  background: var(--bg-2);
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-dim);
}

/* ---------------- Mounted module frame ---------------- */

/* On a tool page the shell stops being a scrolling document and becomes a
   fixed frame: topbar and sidebar hold still, and only the module scrolls.
   Without this the page would grow two scrollbars — the outer document's and
   the frame's — and the sidebar would drift out of view. */
.body--module { overflow: hidden; }
.body--module .app { height: 100dvh; }
.body--module .main { padding: 0; overflow: hidden; }
.body--module .sidenav { overflow-y: auto; }

.module-mount { height: 100%; }
.module-frame {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
  /* The module paints its own background; matching it here stops a flash of
     shell background while the frame document loads. */
  background: var(--bg);
}

/* Standalone framed documents (the not-built-yet placeholder) still need the
   page padding the shell would otherwise have provided. */
.framebody { padding: var(--space-6) var(--space-4); }

/* ==================================================================
   Utilities

   These exist because the portal runs under `style-src 'self'` with no
   'unsafe-inline'. A style="" attribute in the markup is blocked by the
   browser, so spacing that would otherwise be written inline lives here.
   ================================================================== */

.mt-2 { margin-top: var(--space-2); }
.mt-3 { margin-top: var(--space-3); }
.mt-4 { margin-top: var(--space-4); }
.mt-5 { margin-top: var(--space-5); }
.mt-6 { margin-top: var(--space-6); }
.mt-7 { margin-top: var(--space-7); }
.mb-2 { margin-bottom: var(--space-2); }
.mb-4 { margin-bottom: var(--space-4); }
.mb-5 { margin-bottom: var(--space-5); }

.center { text-align: center; }
.muted { color: var(--text-dim); font-size: 0.9rem; }
.small { font-size: 0.85rem; }
.push-right { margin-left: auto; }

.wrap--narrow { max-width: 720px; }
.wrap--form { max-width: 620px; margin-top: 12vh; }

.row {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: wrap;
}

.field-grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

.input--inline { width: 88px; min-height: 36px; }
.select--inline { width: auto; min-height: 36px; }

.ok-tick {
  color: var(--green);
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.off-note { color: var(--text-faint); }

.auth-badge {
  display: inline-grid;
  place-items: center;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: var(--yellow-soft);
  color: var(--yellow);
  margin-bottom: var(--space-4);
}

/* ==================================================================
   Motion & print
   ================================================================== */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .sidenav { transition: none; }
}

@media print {
  .topbar, .sidenav, .drawer-scrim, .skip-link { display: none !important; }
  .app { display: block; }
  .main { padding: 0; }
}

/* 404 numeral: large, tight, and low-contrast so it reads as texture
   rather than competing with the message. */
.notfound__code {
  font-family: var(--mono);
  font-size: 3.5rem;
  font-weight: 600;
  letter-spacing: var(--track-display);
  line-height: 1;
  color: var(--text-faint);
  opacity: 0.5;
  margin-bottom: var(--space-4);
  font-variant-numeric: tabular-nums;
}

/* ---------------- Password generator ----------------
   The temporary password is generated, read off the screen and handed over, so
   the field wants width and the two actions want to be plainly labelled — not
   icon buttons tucked inside it. */
.pwgen { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.pwgen__input {
  flex: 1 1 18rem;
  min-width: 0;
  font-family: var(--mono);
  /* A generated password is scanned character by character when it is read
     aloud or typed; tracking makes that possible. */
  letter-spacing: 0.06em;
}
.pwgen__btn { flex: 0 0 auto; }

/* ---------------- Client domain assignment ----------------
   Sits inside the users table, so it has to stay compact: a client with six
   domains must not make its row six times taller than its neighbours. */
.domainlist {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
  min-width: 0;
}
/* The assigned chips wrap among themselves; the add control keeps its own line
   so a client with several domains does not push it out of reach. */
.domainlist > .domainchip { align-self: flex-start; }

/* The control that changes assignments, opened on demand. Closed it is one
   short word, which is what keeps the table inside the viewport. */
.domainedit summary { list-style: none; }
.domainedit summary::-webkit-details-marker { display: none; }
.domainedit__toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text-dim);
  font-size: 12px;
  font-weight: 550;
  cursor: pointer;
  transition: color var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out);
}
.domainedit__toggle:hover { color: var(--text); border-color: var(--hover-line); }
.domainedit__toggle:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--brand-2);
}
.domainedit[open] .domainedit__toggle { color: var(--text); border-color: var(--hover-line); }

.domainadd { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-2); }
/* The select sizes to its content rather than filling the cell — domain names
   are short, and letting it grow pushed the table past the viewport. The button
   does not shrink, which is what was putting "Add" on its own line. */
.domainadd .select, .domainadd .input { flex: 0 1 auto; min-width: 0; max-width: 12rem; }
.domainadd .btn { flex: 0 0 auto; }

.domainchip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.15rem 0.2rem 0.15rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
  font-size: 12px;
  color: var(--text);
  /* A domain is one token; breaking "client-a.example" across two lines
     made it read as two. */
  white-space: nowrap;
}
/* The remove control is a submit button in its own form — a link would make an
   authorisation change reachable by a GET, and therefore by a prefetch. */
.domainchip__x {
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--text-faint);
  cursor: pointer;
}
.domainchip__x:hover { color: var(--red); background: var(--red-soft); }
.domainchip__x:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--brand-2); }

/* The allowance + status + Save row in the same table. Without a floor on the
   cell it collapses to the width of its longest control and wraps Save onto a
   line of its own, which reads as a second, separate action. */
.table td .row { flex-wrap: nowrap; min-width: 14rem; }

/* A date is one token. Letting "2026-08-21" break after the year made every
   row two lines tall and read as two different values. */
.table td.tnum, .table td.num { white-space: nowrap; }
.table td .row .btn { flex: 0 0 auto; }

/* A select sized to sit inline beside a small button, for the per-row grant
   control on domain management. */
.select--small {
  padding: 4px 8px;
  font-size: 0.82rem;
  max-width: 16rem;
}
