/* Lightmorphic style - the admin surface. See docs/lightmorphic-style.md
   for the full guide.

   Self-contained: plain CSS served as a static file, no external fonts,
   no CDN, no build step. Solid panels with a hairline border and a soft
   neutral shadow, laid out as unequal bento-style blocks on a calm flat
   background. The accent colour carries meaning (primary actions, the
   active nav item) and is never used for body text - it only ever appears
   as a filled background with dark text, or as a border. Built to WCAG
   2.1 AA contrast and focus-visibility standards. */

@font-face {
  font-family: "Manrope";
  src: url("/static/fonts/manrope/Manrope-Variable.woff2") format("woff2-variations"),
       url("/static/fonts/manrope/Manrope-Variable.woff2") format("woff2");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Lets browser-native chrome (scrollbars, form controls) pick a light
     or dark rendering to match - independent of our own theme below,
     but should always agree with it. */
  color-scheme: light dark;

  --sm-background: #ffffff;
  --sm-foreground: #09090b;
  --sm-card: #ffffff;
  --sm-border: #e4e4e7;
  --sm-input: #e4e4e7;
  --sm-ring: #18181b;

  /* The accent, from the shared system in static/css/accents.css - the
     same one the appearance picker sets, so choosing a colour repaints
     these screens too rather than only the chat half (which is exactly
     why the picker was half-broken before). Brand yellow stays the
     fallback for the "yellow" palette and for any page that somehow
     loads this stylesheet without accents.css. */
  --sm-primary: var(--accent, #fbc711);
  --sm-primary-hover: var(--accent-hover, #ddaf0f);
  --sm-primary-foreground: var(--on-accent, #645007);

  --sm-secondary: #f4f4f5;
  --sm-secondary-hover: #e4e4e7;
  --sm-secondary-foreground: #18181b;

  --sm-muted: #f4f4f5;
  --sm-muted-foreground: #71717a;

  --sm-accent-blue: #1b74bc;
  /* Link text, in the current accent. Light themes need the deep
     container shade - the raw accent (brand yellow) on white is
     around 1.6:1 and unreadable. */
  --sm-link: var(--on-accent-container, #5f4c06);

  --sm-destructive: #cc372d;
  --sm-destructive-hover: #f34236;
  --sm-destructive-bg: #fef2f1;
  --sm-destructive-foreground: #fafafa;

  --sm-success: #38813a;
  --sm-success-bg: #f2f9f3;
  --sm-warning: #8f6c03;
  --sm-warning-bg: #fffbee;
  --sm-info: #1b74bc;
  --sm-info-bg: #f0f8fe;

  /* Brand navy - identity, alongside brand yellow. */
  --sm-brand-navy: #111827;

  --sm-radius: 0.875rem;
  --sm-radius-lg: 0.75rem;
  --sm-panel-radius: 1.375rem;
  --sm-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.04);

  /* Panel surface (light). Solid, not translucent: a panel should be a
     panel, legible over whatever is behind it, with depth coming from a
     hairline border and one soft neutral shadow rather than blur. */
  --sm-panel: #ffffff;
  --sm-panel-raised: #ffffff;
  --sm-panel-border: rgba(24, 24, 40, 0.10);
  --sm-shadow-panel: 0 1px 2px rgba(24, 24, 40, 0.05), 0 8px 24px -16px rgba(24, 24, 40, 0.18);
}

/* Auto: follows the OS/browser preference until the visitor picks one
   explicitly with the theme-toggle button (below). Dark values sit closer
   together (card only a shade lighter than the page background) since a
   shadow barely reads against a dark page - the glow does the work of
   separating a panel from the page here instead. */
@media (prefers-color-scheme: dark) {
  :root {
    --sm-background: #09090b;
    --sm-foreground: #fafafa;
    --sm-card: #121214;
    --sm-border: #27272a;
    --sm-input: #3f3f46;
    --sm-ring: #d4d4d8;

    --sm-secondary: #27272a;
    --sm-secondary-hover: #313136;
    --sm-secondary-foreground: #fafafa;

    --sm-muted: #1c1c1f;
    --sm-muted-foreground: #a1a1aa;

    --sm-accent-blue: #2295f1;
    --sm-link: var(--accent, #fbc711);

    --sm-destructive: #f34236;
    --sm-destructive-hover: #f34236;
    --sm-destructive-bg: #350f0c;
    --sm-destructive-foreground: #fafafa;

    --sm-success: #4bae4f;
    --sm-success-bg: #102611;
    --sm-warning: #ffc006;
    --sm-warning-bg: #382a01;
    --sm-info: #2295f1;
    --sm-info-bg: #072135;

    --sm-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);

    --sm-panel: #1b1d29;
    --sm-panel-raised: #222432;
    --sm-panel-border: rgba(255, 255, 255, 0.09);
    --sm-shadow-panel: 0 1px 2px rgba(0, 0, 0, 0.35), 0 8px 24px -16px rgba(0, 0, 0, 0.6);
  }
}

/* Manual: the theme-toggle button stamps data-theme on <html> (see
   admin.js) and localStorage remembers it - these always win over the
   media query above regardless of source order, since an attribute
   selector on :root is more specific than a plain :root. */
:root[data-theme="light"] {
  color-scheme: light;
  --sm-background: #ffffff;
  --sm-foreground: #09090b;
  --sm-card: #ffffff;
  --sm-border: #e4e4e7;
  --sm-input: #e4e4e7;
  --sm-ring: #18181b;

  --sm-secondary: #f4f4f5;
  --sm-secondary-hover: #e4e4e7;
  --sm-secondary-foreground: #18181b;

  --sm-muted: #f4f4f5;
  --sm-muted-foreground: #71717a;

  --sm-accent-blue: #1b74bc;
  --sm-link: var(--on-accent-container, #5f4c06);

  --sm-destructive: #cc372d;
  --sm-destructive-hover: #f34236;
  --sm-destructive-bg: #fef2f1;
  --sm-destructive-foreground: #fafafa;

  --sm-success: #38813a;
  --sm-success-bg: #f2f9f3;
  --sm-warning: #8f6c03;
  --sm-warning-bg: #fffbee;
  --sm-info: #1b74bc;
  --sm-info-bg: #f0f8fe;

  --sm-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.04);

  --sm-panel: #ffffff;
  --sm-panel-raised: #ffffff;
  --sm-panel-border: rgba(24, 24, 40, 0.10);
  --sm-shadow-panel: 0 1px 2px rgba(24, 24, 40, 0.05), 0 8px 24px -16px rgba(24, 24, 40, 0.18);
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --sm-background: #09090b;
  --sm-foreground: #fafafa;
  --sm-card: #121214;
  --sm-border: #27272a;
  --sm-input: #3f3f46;
  --sm-ring: #d4d4d8;

  --sm-secondary: #27272a;
  --sm-secondary-hover: #313136;
  --sm-secondary-foreground: #fafafa;

  --sm-muted: #1c1c1f;
  --sm-muted-foreground: #a1a1aa;

  --sm-accent-blue: #2295f1;
  --sm-link: var(--accent, #fbc711);

  --sm-destructive: #f34236;
  --sm-destructive-hover: #f34236;
  --sm-destructive-bg: #350f0c;
  --sm-destructive-foreground: #fafafa;

  --sm-success: #4bae4f;
  --sm-success-bg: #102611;
  --sm-warning: #ffc006;
  --sm-warning-bg: #382a01;
  --sm-info: #2295f1;
  --sm-info-bg: #072135;

  --sm-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);

  --sm-panel: #1b1d29;
  --sm-panel-raised: #222432;
  --sm-panel-border: rgba(255, 255, 255, 0.09);
  --sm-shadow-panel: 0 1px 2px rgba(0, 0, 0, 0.35), 0 8px 24px -16px rgba(0, 0, 0, 0.6);
}

* { box-sizing: border-box; }

html, body {
  overflow-x: hidden;
}

body {
  margin: 0;
  font-family: "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif;
  background: var(--sm-background);
  color: var(--sm-foreground);
  line-height: 1.5;
  position: relative;
}


a { color: var(--sm-link); }

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--sm-primary);
  color: var(--sm-primary-foreground);
  padding: 0.75rem 1rem;
  z-index: 100;
}
.skip-link:focus {
  left: 0.5rem;
  top: 0.5rem;
}

/* A real outline with an offset, not a box-shadow ring.
   
   The shadow version existed because outlines used not to follow
   border-radius, so a ring round a pill badge or the circular theme toggle
   came out square. Every current browser follows the radius now, and the
   outline has two advantages the shadow cannot match: it survives forced-
   colors / Windows High Contrast mode, where box-shadows are dropped
   entirely and focus would simply disappear, and it does not fight any
   component that wants a shadow of its own.

   Deliberately --sm-ring, not the accent. The style guide's wording asks
   for an accent outline, but brand yellow on white measures 1.58:1 and the
   non-text contrast floor is 3:1 - so following that sentence literally
   would fail the guide's own accessibility section, which it says is "not
   negotiable on a deadline". The ring token is 17.7:1 in light and inverts
   for dark. Where two rules in the guide disagree, the accessibility floor
   wins. */
:focus-visible {
  outline: 2px solid var(--sm-ring);
  outline-offset: 2px;
}

.auth-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem 1rem;
}

.auth-card {
  background: var(--sm-panel);
  border: 1px solid var(--sm-panel-border);
  border-radius: var(--sm-panel-radius);
  box-shadow: var(--sm-shadow-panel);
  padding: 2rem;
  width: 100%;
  max-width: 26rem;
}

.auth-card h1 {
  margin-top: 0;
  font-size: 1.5rem;
}

/* Light/dark logo swap: both images are always in the DOM (so there's no
   flash-of-wrong-logo while CSS loads) - auto via the media query, manual
   override via data-theme (same mechanism as the color palette above).
   Element+class selectors here so this always wins over plain single-class
   sizing rules like .auth-card__logo, and the data-theme rules win over
   the media query, regardless of source order in either case. */
img.brand-logo--light { display: block; }
img.brand-logo--dark { display: none; }
@media (prefers-color-scheme: dark) {
  img.brand-logo--light { display: none; }
  img.brand-logo--dark { display: block; }
}
:root[data-theme="light"] img.brand-logo--light { display: block; }
:root[data-theme="light"] img.brand-logo--dark { display: none; }
:root[data-theme="dark"] img.brand-logo--light { display: none; }
:root[data-theme="dark"] img.brand-logo--dark { display: block; }

.auth-card__logo {
  display: block;
  height: 3rem;
  width: auto;
  margin: 0 auto 1.25rem;
}

/* Secondary links under an auth form - "Forgot your password?" and the
   links back to each login. Centred and quiet: they matter to the person
   who needs them and should stay out of the way of everyone else. The
   .hint above it carries a negative top margin for use under a field
   label, which is wrong here, hence the explicit margin. */
.auth-card__aside {
  margin: 1.25rem 0 0;
  text-align: center;
}

.field {
  margin-bottom: 1.25rem;
}

.field label {
  display: block;
  /* Style guide: form labels are 0.9375rem / 600. The app had 1rem / 500
     here and 600 only inside .stacked-form, so the same label rendered two
     different ways depending on which form it landed in. */
  font-size: 0.9375rem;
  font-weight: 600;
  margin-bottom: 0.35rem;
}

.field input,
.field textarea,
.field select {
  width: 100%;
  padding: 0.55rem 0.75rem;
  border: 1px solid var(--sm-input);
  border-radius: var(--sm-radius);
  font-size: 0.95rem;
  font-family: inherit;
  background: var(--sm-background);
  color: var(--sm-foreground);
  transition: border-color 0.15s ease;
}
.field input::placeholder,
.field textarea::placeholder {
  color: var(--sm-muted-foreground);
}

.field .error-text {
  color: var(--sm-destructive);
  font-size: 0.875rem;
  margin-top: 0.35rem;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--sm-primary);
  color: var(--sm-primary-foreground);
  border: 1px solid transparent;
  border-radius: var(--sm-radius);
  padding: 0.5rem 1rem;
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.button:hover { background: var(--sm-primary-hover); }
.button:disabled,
.link-button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
.button:disabled:hover { background: var(--sm-primary); }

/* Notifications, per the style guide: "Success and error messages slide in
   from the side as a panel - never a green banner". These used to be tinted
   blocks pushed into the top of the page content, which meant a solid green
   or red panel AND a layout shift on every save.

   Panel styling like everything else - panel background, standard border,
   the one shadow - so what carries the meaning is the word, not the colour.
   A thin coloured edge is the only tint, and it is never the only signal:
   every toast is prefixed with "Done" / "Problem" / "Note". */
.flash-list {
  list-style: none;
  margin: 0;
  padding: 0;
  position: fixed;
  /* From the right edge, not the bottom. */
  top: 1rem;
  right: 1rem;
  width: min(24rem, calc(100vw - 2rem));
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  /* Same layer as tooltips: nothing may cover a message telling you
     something failed. */
  z-index: 2147483000;
  pointer-events: none;
}
.flash {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
  background: var(--sm-panel);
  color: var(--sm-foreground);
  border: 1px solid var(--sm-panel-border);
  border-left-width: 3px;
  border-radius: var(--sm-radius);
  box-shadow: var(--sm-shadow-panel);
  padding: 0.75rem 1rem;
  margin: 0;
  font-size: 0.9375rem;
  line-height: 1.45;
  animation: flash-in 0.25s ease-out both;
}
/* The word, not the colour, is what tells you which kind this is. */
.flash::before {
  font-weight: 700;
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  padding-top: 0.15rem;
}
.flash--success { border-left-color: var(--sm-success); }
.flash--success::before { content: "Done"; color: var(--sm-success); }
.flash--error { border-left-color: var(--sm-destructive); }
.flash--error::before { content: "Problem"; color: var(--sm-destructive); }
.flash--info { border-left-color: var(--sm-info); }
.flash--info::before { content: "Note"; color: var(--sm-info); }
.flash--warning { border-left-color: var(--sm-warning); }
.flash--warning::before { content: "Careful"; color: var(--sm-warning); }

.flash-dismiss {
  margin-left: auto;
  background: none;
  border: 0;
  padding: 0 0 0 0.5rem;
  color: var(--sm-muted-foreground);
  font-size: 1.125rem;
  line-height: 1;
  cursor: pointer;
}
.flash-dismiss:hover { color: var(--sm-foreground); }
.flash-dismiss:focus-visible { outline: 2px solid var(--sm-ring); outline-offset: 2px; }

@keyframes flash-in {
  from { opacity: 0; transform: translateX(1.5rem); }
  to { opacity: 1; transform: translateX(0); }
}
/* "Every animation respects prefers-reduced-motion and either slows down or
   turns off entirely." Here it turns off - the toast still appears. */
@media (prefers-reduced-motion: reduce) {
  .flash { animation: none; }
}

@media (max-width: 30rem) {
  /* Still from the top rather than the bottom, but full width - a 24rem
     panel with a 1rem margin does not fit a phone. */
  .flash-list { left: 1rem; right: 1rem; width: auto; }
}

.qr-block {
  text-align: center;
  margin-bottom: 1.5rem;
}
.qr-block img {
  max-width: 220px;
  width: 100%;
  height: auto;
  border: 1px solid var(--sm-border);
  border-radius: var(--sm-radius);
}
.qr-block .secret {
  font-family: ui-monospace, monospace;
  word-break: break-all;
}

.app-shell {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* A floating bar rather than a flush edge-to-edge header - the same
   "panel with visible air around it" treatment used for the sidebar,
   adapted to a horizontal top bar. Used by the client portal and the
   pre-login screens; the operator shell uses the sidebar instead. */
.app-header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  background: var(--sm-panel);
  border: 1px solid var(--sm-panel-border);
  border-radius: var(--sm-panel-radius);
  box-shadow: var(--sm-shadow-panel);
  margin: 1rem 1rem 0;
  padding: 0.85rem 1.5rem;
  position: sticky;
  top: 1rem;
  z-index: 50;
}

.app-header__row {
  display: contents;
}

.app-header__brand {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 600;
  font-size: 1.1rem;
  color: var(--sm-foreground);
  text-decoration: none;
}
.app-header__brand img {
  height: 1.75rem;
  width: auto;
}

.app-header__nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  flex: 1;
  min-width: 0;
}
/* Desktop only - centers the links within the space between the brand and
   the theme-toggle/logout actions, which stay put on the right. Mobile's
   collapsed menu (see the max-width: 700px block below) stays left-aligned
   as a plain vertical list - centering a stack of full-width rows wouldn't
   read as "centered" the way it does in a single horizontal row. */
@media (min-width: 701px) {
  .app-header__nav {
    justify-content: center;
  }
}
.app-header__nav a {
  text-decoration: none;
  color: var(--sm-muted-foreground);
  padding: 0.4rem 0.1rem;
  border-bottom: 2px solid transparent;
  font-size: 0.9rem;
}
.app-header__nav a[aria-current="page"] {
  color: var(--sm-foreground);
  border-bottom-color: var(--sm-primary);
  font-weight: 600;
}

/* Hidden on wide screens - the nav/actions row has room to just sit inline
   next to the brand (see .app-header's own flex-wrap above). */
.app-header__menu-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: 1px solid var(--sm-border);
  border-radius: var(--sm-radius);
  background: var(--sm-card);
  color: var(--sm-foreground);
  cursor: pointer;
}
.app-header__menu-toggle svg {
  width: 1.35rem;
  height: 1.35rem;
}

.app-header__actions {
  display: flex;
  align-items: center;
  gap: 0.85rem;
}

/* Below this width, five nav links + theme toggle + logout no longer fit
   next to the brand on one line - the old behavior (plain flex-wrap) just
   let everything reflow into a cramped, oddly-ordered stack that ate most
   of a phone screen's height before any real content appeared. A collapsed
   hamburger menu (matching the "both variants always in the DOM, CSS/one
   class decides what's visible" show/hide technique used everywhere else
   in this app - the theme toggle, the maintenance page toggle) keeps the
   collapsed header to a single compact row. Placed after the base
   .app-header__nav/.app-header__actions rules above (not just inside a
   media query interspersed earlier in the file) so its `display: none`
   actually wins the cascade at this width, rather than losing to a
   same-specificity declaration that happens to come later in the file. */
@media (max-width: 700px) {
  .app-header {
    flex-wrap: nowrap;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    margin: 0.5rem 0.5rem 0;
    padding: 0.6rem 1rem;
  }
  .app-header__row {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .app-header__menu-toggle {
    display: inline-flex;
  }
  .app-header__nav,
  .app-header__actions {
    display: none;
    flex-direction: column;
    align-items: stretch;
    flex: none;
    width: 100%;
    gap: 0;
  }
  .app-header__nav a {
    padding: 0.75rem 0.25rem;
    border-bottom: 1px solid var(--sm-border);
  }
  .app-header__nav a[aria-current="page"] {
    border-bottom: 1px solid var(--sm-border);
    background: var(--sm-muted);
  }
  .app-header__actions {
    flex-direction: row;
    justify-content: space-between;
    padding: 0.75rem 0.25rem 0.25rem;
  }
  .app-header--menu-open .app-header__nav,
  .app-header--menu-open .app-header__actions {
    display: flex;
  }
}

.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: 1px solid var(--sm-border);
  border-radius: 999px;
  background: var(--sm-card);
  color: var(--sm-muted-foreground);
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease;
}
.theme-toggle:hover { background: var(--sm-muted); color: var(--sm-foreground); }
.theme-toggle svg { width: 1.15rem; height: 1.15rem; }

/* Two buttons, one per direction - same show/hide mechanism as the color
   palette and logo above, so the visible one (and therefore its baked-in
   aria-label) is always correct on first paint with zero JS needed for
   that part; JS only has to handle the click itself. */
.theme-toggle--to-dark { display: inline-flex; }
.theme-toggle--to-light { display: none; }
@media (prefers-color-scheme: dark) {
  .theme-toggle--to-dark { display: none; }
  .theme-toggle--to-light { display: inline-flex; }
}
:root[data-theme="light"] .theme-toggle--to-dark { display: inline-flex; }
:root[data-theme="light"] .theme-toggle--to-light { display: none; }
:root[data-theme="dark"] .theme-toggle--to-dark { display: none; }
:root[data-theme="dark"] .theme-toggle--to-light { display: inline-flex; }

.auth-page-toggle {
  position: fixed;
  top: 1rem;
  right: 1rem;
}

.app-main {
  flex: 1;
  padding: 2rem 1.5rem;
  max-width: 72rem;
  width: 100%;
  margin: 0 auto;
}

/* Every top-level block on a screen gets the guide's 20px of air, whatever
   it is. This used to be `.app-main > section.card { margin-bottom }`,
   which covered exactly one class - so a .card-grid or a .table-scroll
   sitting between two panels touched whatever came after it, and the more
   kinds of block a screen used the more places it happened.

   A margin on every child rather than flex + gap on the parent: flex would
   change the layout context for everything on every admin screen (stretch,
   shrink, and the loss of margin collapsing inside), which is a lot of
   invisible change to make at once for a spacing fix. Sibling margins
   collapse to the larger of the two, so a block bringing its own margin
   still gets at least this much and never twice as much. */
.app-main > * { margin-bottom: 1.25rem; }
.app-main > *:last-child { margin-bottom: 0; }

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1.25rem;
}

/* Bento layout for the dashboard specifically (a known, fixed set of
   panels, unlike .card-grid's variable-count usage elsewhere) - two
   medium tiles side by side, one wide tile below, rather than one more
   uniform row of equal-width cards. See admin/dashboard.html. */
.bento-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1.25rem;
}
.bento-grid > .card:nth-child(1) { grid-column: span 2; }
.bento-grid > .card:nth-child(2) { grid-column: span 2; }
.bento-grid > .card:nth-child(3) { grid-column: span 4; }
@media (max-width: 700px) {
  .bento-grid { grid-template-columns: 1fr; }
  .bento-grid > .card { grid-column: span 1 !important; }
}

/* Style guide type scale. There was no h1 rule at all, so page titles ran
   on the browser default - which is not 2rem in every context: inside a
   <section>, older HTML5 outline behaviour shrinks a nested h1, so the same
   title rendered at different sizes on different screens.

   Negative letter-spacing on the large heading only, never on body text. */
h1 {
  font-size: 2rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin-top: 0;
}
h2 {
  font-size: 1.75rem;
  font-weight: 600;
  line-height: 1.25;
}

.card {
  background: var(--sm-panel);
  border: 1px solid var(--sm-panel-border);
  border-radius: var(--sm-panel-radius);
  box-shadow: var(--sm-shadow-panel);
  padding: 1.25rem;
}
.card h2 {
  margin-top: 0;
  /* Style guide: panel heading is 1.0625rem / 600. */
  font-size: 1.0625rem;
  font-weight: 600;
}

.stat-card {
  text-align: center;
}
.stat-value {
  font-size: 2.25rem;
  font-weight: 700;
  color: var(--sm-link);
  line-height: 1.1;
}
.stat-label {
  color: var(--sm-muted-foreground);
  font-size: 0.875rem;
  margin-top: 0.25rem;
}

.trend-chart {
  width: 100%;
  min-width: 22rem;
  height: auto;
  max-height: 12rem;
}
.trend-chart__bar {
  fill: var(--accent, #fbc711);
}
.trend-chart__count {
  fill: var(--sm-muted-foreground);
  font-size: 10px;
}
.trend-chart__label {
  fill: var(--sm-muted-foreground);
  font-size: 10px;
}

.trend-up { color: var(--sm-success); }
.trend-down { color: var(--sm-destructive); }

.hint {
  color: var(--sm-muted-foreground);
  /* Style guide: hint text is 0.8125rem, muted. */
  font-size: 0.8125rem;
  margin-top: -0.5rem;
}

.donut-row {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  flex-wrap: wrap;
}
.donut-chart {
  width: 8.75rem;
  height: 8.75rem;
  flex: none;
}
.chart-legend {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 0.875rem;
}
.chart-legend li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.15rem 0;
}
.chart-legend__swatch {
  display: inline-block;
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 3px;
  flex: none;
}

.table-scroll {
  overflow-x: auto;
  border: 1px solid var(--sm-border);
  border-radius: var(--sm-radius-lg);
  margin-bottom: 1rem;
}

table {
  width: 100%;
  min-width: 32rem;
  border-collapse: collapse;
  background: var(--sm-card);
}
th, td {
  text-align: left;
  padding: 0.65rem 0.85rem;
  border-bottom: 1px solid var(--sm-border);
}
th {
  color: var(--sm-muted-foreground);
  font-size: 0.8rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
tbody tr:hover td { background: var(--sm-muted); }
tbody tr:last-child td { border-bottom: none; }

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.15rem 0.6rem;
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  /* A badge hugs its text. As a direct child of a flex-column card the
     default cross-axis stretch pulled it out to the full card width,
     which read as a broken input, not a status. */
  width: fit-content;
}
.badge--active { background: var(--sm-success-bg); color: var(--sm-success); border-color: var(--sm-success); }
.badge--paused { background: var(--sm-warning-bg); color: var(--sm-warning); border-color: var(--sm-warning); }
.badge--maintenance { background: var(--sm-warning-bg); color: var(--sm-warning); border-color: var(--sm-warning); }
.badge--deleted { background: var(--sm-destructive-bg); color: var(--sm-destructive); border-color: var(--sm-destructive); }

/* Client-vs-own-site switch (client_detail.html's Site card). Left label
   is the "off" state (a regular client), right is "on" (one of Charlie's
   own) - matches how the switch itself reads left-to-right. */
.owner-switch-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 4px;
}
.owner-switch-label {
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--sm-foreground);
}
.owner-switch-label--dim {
  font-weight: 400;
  color: var(--sm-muted-foreground);
}
.owner-switch {
  position: relative;
  flex-shrink: 0;
  width: 44px;
  height: 24px;
  padding: 0;
  border: 1px solid var(--sm-border);
  border-radius: 999px;
  background: var(--sm-muted);
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}
.owner-switch[aria-checked="true"] {
  background: var(--sm-primary);
  border-color: var(--sm-primary);
}
.owner-switch-thumb {
  position: absolute;
  top: 1px;
  left: 1px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--sm-card);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
  transition: transform 0.15s ease;
}
.owner-switch[aria-checked="true"] .owner-switch-thumb {
  transform: translateX(20px);
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.25rem;
}

.breadcrumb-back {
  margin: 0 0 0.75rem;
  font-size: 0.9rem;
}
.breadcrumb-back a {
  color: var(--sm-muted-foreground);
  text-decoration: none;
}
.breadcrumb-back a:hover {
  color: var(--sm-foreground);
  text-decoration: underline;
}

/* Superseded by .app-main's own gap - a margin here would double it. Kept
   as a no-op comment rather than deleted so the next person searching for
   "why do cards have space" lands on the container rule above. */

.form-card {
  background: var(--sm-panel);
  border: 1px solid var(--sm-panel-border);
  border-radius: var(--sm-panel-radius);
  box-shadow: var(--sm-shadow-panel);
  padding: 1.25rem;
  max-width: 32rem;
}

/* The blog/newsletter editor: a writing canvas, not a settings form.
   The panel opens to the full content width and the editing surface gets
   real height - Editor.js centres its own content column at roughly email
   width (~650px) inside it, so what you compose is shaped like what a
   subscriber's inbox will show. */
.form-card--canvas { max-width: none; }
.blog-editor-canvas {
  background: var(--sm-background);
  border: 1px solid var(--sm-input);
  border-radius: var(--sm-radius);
  padding: 1rem;
  min-height: 65vh;
}

.field--checkbox {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.field--checkbox input { width: auto; }
.field--checkbox label { margin-bottom: 0; }

.inline-form {
  display: flex;
  align-items: flex-end;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.inline-form .field { margin-bottom: 0; }
.inline { display: inline; }

.button-row {
  display: flex;
  gap: 0.75rem;
}

/* Outline button variant: transparent background, a real border, filling
   in with a soft neutral tint on hover. */
.button--secondary {
  background: var(--sm-background);
  color: var(--sm-foreground);
  border: 1px solid var(--sm-input);
}
.button--secondary:hover { background: var(--sm-muted); }

/* Resting state is a soft tint + colored border, not a solid fill -
   solidifying only on hover keeps a merely-visible danger button from
   reading as urgent until it's actually about to be clicked. */
.button--danger {
  background: var(--sm-destructive-bg);
  color: var(--sm-destructive);
  border: 1px solid var(--sm-destructive);
}
.button--danger:hover { background: var(--sm-destructive); color: var(--sm-destructive-foreground); }

/* Armed "click again to confirm" state (see admin.js) - overrides whatever
   base button variant it's layered on (.button, .button--secondary,
   .button--danger, .link-button) to the same unmistakable solid red,
   regardless of that variant's own resting/hover colors. */
.button--confirming,
.button--confirming:hover {
  background: var(--sm-destructive) !important;
  color: var(--sm-destructive-foreground) !important;
  border-color: var(--sm-destructive) !important;
  text-decoration: none !important;
  animation: confirm-pulse 1s ease-in-out infinite;
}
/* .link-button has no padding/radius of its own (a bare inline text
   trigger) - give it the same solid-button footprint as the others once
   armed, so "click again" reads as a button, not a highlighted word. */
.link-button.button--confirming {
  padding: 0.2rem 0.6rem;
  border-radius: var(--sm-radius);
}
@keyframes confirm-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.75; }
}
@media (prefers-reduced-motion: reduce) {
  .button--confirming { animation: none; }
}

pre {
  white-space: pre-wrap;
  background: var(--sm-muted);
  border: 1px solid var(--sm-border);
  border-radius: var(--sm-radius);
  padding: 1rem;
  font-family: ui-monospace, monospace;
  font-size: 0.9rem;
}

.email-preview-frame {
  display: block;
  width: 100%;
  height: 70vh;
  min-height: 32rem;
  border: 1px solid var(--sm-border);
  border-radius: var(--sm-radius);
  background: #f4f6fb;
}

.email-preview-text {
  margin-top: 0.75rem;
}
.email-preview-text summary {
  cursor: pointer;
  color: var(--sm-muted-foreground);
  font-size: 0.9rem;
}

.details-list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 0.9rem;
}
.details-list li { margin-bottom: 0.15rem; }

.link-button {
  background: none;
  border: none;
  padding: 0;
  color: var(--sm-destructive);
  text-decoration: underline;
  cursor: pointer;
  font-size: 0.95rem;
}

/* --- full-width form fields ------------------------------------------- */
/* The chat settings textareas (link email body, first chat message, FAQ)
   were rendering at the browser's default ~20-character width - a tiny box
   for what is often several paragraphs of copy. These fill the panel and
   are comfortably tall, and .stacked-form gives each field real breathing
   room instead of the cramped <p>-wrapped stack they were in. */
.field-full {
  display: block;
  width: 100%;
  box-sizing: border-box;
  padding: 0.7rem 0.85rem;
  font: inherit;
  line-height: 1.55;
  color: var(--sm-foreground);
  background: var(--sm-input);
  border: 1px solid var(--sm-border);
  border-radius: var(--sm-radius);
  resize: vertical;
}
.field-full:focus-visible {
  outline: 2px solid var(--sm-primary);
  outline-offset: 1px;
  border-color: var(--sm-primary);
}
textarea.field-full { min-height: 7rem; }

.stacked-form { display: flex; flex-direction: column; gap: 1.4rem; align-items: flex-start; }
.stacked-form .field { display: flex; flex-direction: column; gap: 0.4rem; width: 100%; }
.stacked-form .field label { font-weight: 600; }
.stacked-form .field .hint { margin: 0; }
/* align-items:center is inherited from .field--checkbox, where it centres
   vertically because that rule is a ROW. Switching to a column here made it
   centre horizontally instead, which pushed the checkbox and its hint into
   the middle of the form. */
.stacked-form .field--checkbox { flex-direction: column; align-items: flex-start; }
.stacked-form .field--checkbox label { font-weight: 500; display: flex; align-items: center; gap: 0.5rem; }
.stacked-form .field--checkbox .hint { margin-top: 0.25rem; }

.error-text { color: var(--sm-destructive); font-size: 0.875rem; }

/* --- dashboard stats + health checks ---------------------------------- */
.stat-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem 2.5rem;
  margin: 0 0 1.25rem;
}
.stat { display: flex; flex-direction: column; gap: 0.15rem; }
.stat__value { font-size: 1.75rem; font-weight: 700; line-height: 1.1; color: var(--sm-foreground); }
.stat__value--warn { color: var(--sm-warning); }
.stat__label { font-size: 0.8125rem; color: var(--sm-muted-foreground); }

.check-list { list-style: none; margin: 0 0 1rem; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.check-list li { display: flex; gap: 0.55rem; align-items: baseline; font-size: 0.9375rem; }
/* A leading mark rather than colour alone - colour on its own fails for
   anyone who can't distinguish it, and this list is the "is anything
   wrong?" summary, so it has to be readable without it. */
.check-list li::before { font-weight: 700; }
.check--ok::before { content: "✓"; color: var(--sm-success); }
.check--warn::before { content: "!"; color: var(--sm-warning); }

/* Explanatory text that introduces a group of buttons, rather than the
   .hint that trails a form field. .hint carries margin-top:-0.5rem to sit
   tight under a label, which pulled this copy up against the button above
   it and made it read as part of that button. */
.section-note {
  color: var(--sm-muted-foreground);
  font-size: 0.9375rem;
  line-height: 1.6;
  margin: 0 0 1rem;
  max-width: 62ch;
}

/* A field switched off by the checkbox above it (see admin.js). Dimmed but
   still legible and still submitted - the text is not being thrown away,
   it just has nothing using it right now. */
.field--inactive {
  opacity: 0.55;
}
.field--inactive textarea,
.field--inactive input {
  background: var(--sm-muted);
  cursor: not-allowed;
}

/* A titled block inside a card - status, then details, then actions - so a
   long section reads as a few clear parts rather than one run of badge,
   paragraph, form, note and buttons with nothing separating them. */
/* The card's own gap already separates blocks - this only adds the rule
   line and a little air around it. The old 1.75rem + 1.5rem stacked on
   top of that gap and opened ~43px holes between sections. */
.detail-block + .detail-block {
  margin-top: 0.25rem;
  padding-top: 1.125rem;
  border-top: 1px solid var(--sm-border);
}
.detail-block h3 {
  margin: 0 0 0.35rem;
  font-size: 1rem;
  font-weight: 600;
}
.detail-block > p:first-child,
.detail-block > .badge + p {
  margin-top: 0.5rem;
}
.detail-block .section-note {
  margin-bottom: 1rem;
}

/* ---- Card layout, made consistent -----------------------------------
   These panels grew one addition at a time and drifted: explanatory text
   sometimes above the controls and sometimes below, .hint (which carries a
   negative top margin, for sitting tight under a FIELD LABEL) used as
   section copy so it collided with the heading above it or jammed under
   the button it followed, and paragraphs running the full card width so
   some lines were very long next to others only a few words wide.

   One pattern now: heading, then a note at a readable measure, then the
   controls. .hint goes back to its one job - help text under a field. */

.card {
  /* Owns the rhythm between its own children, instead of each element
     bringing its own margins and colliding with the next. */
  display: flex;
  flex-direction: column;
  gap: 0.875rem;
  padding: 1.5rem;
}
.card > * { margin-top: 0; margin-bottom: 0; }

.card h2 {
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.3;
}
.card h3 {
  font-size: 0.9375rem;
  font-weight: 600;
  margin-top: 0.5rem;
}

/* Body copy inside a card. ~68 characters is the readable measure - past
   that the eye loses its place returning to the next line, which is what
   made these panels feel like a wall of long lines. */
.card > p,
.card .section-note,
.card > ul {
  max-width: 68ch;
  line-height: 1.6;
}

/* A row of buttons is a block in its own right, so it needs air above it
   rather than inheriting whatever margin the previous element had. */
.card .button-row {
  flex-wrap: wrap;
  align-items: center;
  margin-top: 0.25rem;
}

/* .hint is for under a field, and only there. Neutralised at card level so
   an existing one used as section copy can't pull itself into the heading
   above; the .field rule below restores the tight fit where it belongs. */
.card > .hint {
  margin-top: 0;
  color: var(--sm-muted-foreground);
  font-size: 0.9375rem;
  line-height: 1.6;
  max-width: 68ch;
}
.field .hint,
.field small.hint {
  display: block;
  margin-top: 0.25rem;
}

/* Forms inside a card carry their own internal spacing, so they don't need
   the card's gap doubled up on top. */
.card .stacked-form,
.card .inline-form {
  margin-top: 0.25rem;
}

/* ================= Controls: one size, one system ====================
   Measured on the client page before this: button.button was 34px tall,
   a.button 39px, a text input 41px and a file input 43px - four heights
   for controls that sit next to each other. Fonts differed too (14px vs
   15.2px).

   Worse, .button--secondary used var(--sm-background) - the PAGE
   background - so in dark mode it rendered rgb(9,9,11) on a rgb(9,9,11)
   page. A black button on a black background, visible only by its border.
   It now uses --sm-secondary, the raised-surface tone, which is a real
   step away from the page in both themes.

   Everything a user can click or type into is now one height, one font
   size and one radius. Colour carries meaning: brand yellow for the
   primary action, a raised neutral for secondary, red for destructive. */

:root {
  --sm-control-height: 2.5rem;
  --sm-control-font: 0.9375rem;
  --sm-control-padding-x: 1rem;
}

.button,
a.button,
button.button,
input[type="submit"].button,
.field input,
.field select,
input[type="search"],
select {
  min-height: var(--sm-control-height);
  font-size: var(--sm-control-font);
  font-family: inherit;
  border-radius: var(--sm-radius);
  line-height: 1.2;
  box-sizing: border-box;
}

.button,
a.button,
button.button,
input[type="submit"].button {
  padding: 0 var(--sm-control-padding-x);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}

.field input,
.field select,
input[type="search"],
select {
  padding: 0 0.75rem;
}

/* Secondary: a raised surface, not the page it sits on. */
.button--secondary {
  background: var(--sm-secondary);
  color: var(--sm-secondary-foreground);
  border: 1px solid var(--sm-border);
}
.button--secondary:hover {
  background: var(--sm-secondary-hover);
  border-color: var(--sm-muted-foreground);
}

/* Destructive: solid red and white, so it never reads as just another
   neutral button in a row of them. */
/* A fixed red in both themes, not var(--sm-destructive): that token is
   #f34236 in dark mode, and white on it measures 3.72:1 - below the 4.5
   floor. #cc372d gives 5.05:1, and the darker hover 6.29:1. The palette's
   red is unchanged; this is the shade of it that white text can sit on. */
.button--danger {
  background: #cc372d;
  color: #ffffff;
  border: 1px solid #cc372d;
}
.button--danger:hover {
  background: #b32e26;
  border-color: #b32e26;
}

/* The file input's own button is drawn by the browser and ignores
   everything above unless targeted directly - which is why "Choose file"
   never matched anything around it. */
/* The "Choose file" button has to FIT inside the input's content box, and
   the arithmetic has to be done rather than eyeballed - getting it wrong is
   what made the button hang over the top edge of the field:

     input           40px  (--sm-control-height)
     - borders        2px  (1px each side, from .field input)
     - padding       10px  (0.3125rem each side, set below)
     = space         28px  for the button

   So the button is given exactly 28px, border-box, and no min-height that
   could push past it. The generous 0.55rem vertical padding inherited from
   .field input left only 20.4px for a 32px button, hence the overlap.
   Anything that changes --sm-control-height, this padding, or the button
   height must keep those three numbers adding up. */
input[type="file"] {
  min-height: var(--sm-control-height);
  padding-top: 0.3125rem;
  padding-bottom: 0.3125rem;
  font-size: var(--sm-control-font);
  font-family: inherit;
  color: var(--sm-muted-foreground);
  display: flex;
  align-items: center;
  line-height: 1;
}
input[type="file"]::file-selector-button {
  box-sizing: border-box;
  height: calc(var(--sm-control-height) - 0.625rem - 2px);
  margin-right: 0.75rem;
  padding: 0 0.875rem;
  border: 1px solid var(--sm-border);
  border-radius: var(--sm-radius);
  background: var(--sm-secondary);
  color: var(--sm-secondary-foreground);
  font-family: inherit;
  font-size: var(--sm-control-font);
  font-weight: 600;
  cursor: pointer;
  transition: background-color .15s ease, border-color .15s ease;
}
input[type="file"]::file-selector-button:hover {
  background: var(--sm-secondary-hover);
  border-color: var(--sm-muted-foreground);
}

/* Native select arrow, so a dropdown reads as a dropdown without the OS
   drawing its own mismatched chrome on top of our border. */
select {
  appearance: none;
  background-color: var(--sm-background);
  color: var(--sm-foreground);
  border: 1px solid var(--sm-input);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2371717a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.625rem center;
  padding-right: 2.25rem;
}

/* A search field and the button beside it are one row - they have to share
   a baseline, which they can't if one is 41px and the other 34px. */
.filter-row,
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.625rem;
}
.filter-row input,
.filter-row select,
.filter-bar input,
.filter-bar select {
  min-height: var(--sm-control-height);
  font-size: var(--sm-control-font);
  border-radius: var(--sm-radius);
  border: 1px solid var(--sm-input);
  background: var(--sm-background);
  color: var(--sm-foreground);
  padding: 0 0.75rem;
}
.filter-row input[type="search"],
.filter-row input[type="text"],
.filter-bar input[type="search"],
.filter-bar input[type="text"] {
  min-width: 14rem;
  flex: 1 1 14rem;
}

/* ---- Page density -----------------------------------------------------
   Two things made this page feel like a long thin ribbon of content inside
   very wide panels.

   First, .field input is width:100% with no maximum, so on a 72rem page a
   name or email field stretched to about 1100px - a single line of text in
   a box wide enough for a paragraph, with the eye travelling the whole way
   to find the caret. Single-line controls are now capped; textareas keep
   the full width because they genuinely use it.

   Second, all eleven panels stacked in one column, so the page ran several
   screens deep with most of that height half-empty. Small self-contained
   panels now share a row. */

.field input[type="text"],
.field input[type="email"],
.field input[type="url"],
.field input[type="tel"],
.field input[type="password"],
.field input[type="search"],
.field input[type="file"],
.field select {
  max-width: 30rem;
}

/* Wider, because a path or a domain is long and breaking it across the
   box's edge is worse than the extra width. */
.field input[name="target_path"],
.field input[name="domain"] {
  max-width: 38rem;
}

/* The Site card's address list: a couple of labelled links, not an essay -
   default disc bullets read as unstyled HTML inside a designed panel. */
.card > ul {
  list-style: none;
  padding-left: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

/* Widget on/off rows: badge in a fixed column, action beside it, so both
   rows share left edges and both buttons come out the same width instead
   of two ragged stair-steps. */
.toggle-list {
  display: grid;
  /* minmax(0, …) lets the button column shrink below its text's own width
     on narrow screens - the label wraps instead of the button punching
     through the card edge (measured +21px at 768px with plain
     max-content). */
  grid-template-columns: 8.5rem minmax(0, max-content);
  gap: 0.75rem;
}
.toggle-list form {
  /* Both rows share the outer grid's columns, so the button column is
     sized once across the whole list - equal widths, one left edge. */
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
}
.toggle-list .button { width: 100%; }

/* Bento row: small panels side by side rather than each claiming a
   full-width band. auto-fit collapses it to one column on narrow screens
   with no extra media query. */
.card-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
  gap: 1.25rem;
  margin-bottom: 1.25rem;
  align-items: start;
}
.card-row > .card {
  margin-bottom: 0;
  height: 100%;
}

/* The file list can run to hundreds of rows - long enough on its own to
   bury everything below it. Scrolls within its own panel instead. */
.file-table-scroll {
  max-height: 26rem;
  overflow-y: auto;
  /* Sideways too: on narrow screens the table is wider than the panel,
     and without this it physically overhangs the card edge (measured
     +147px at 768px) instead of scrolling within it. */
  overflow-x: auto;
  border: 1px solid var(--sm-border);
  border-radius: var(--sm-radius);
}
.file-table-scroll table { margin: 0; }
.file-table-scroll thead th {
  position: sticky;
  top: 0;
  background: var(--sm-panel);
  z-index: 1;
}

/* A label + control + button on one line should share a baseline, and wrap
   as whole units rather than leaving a button stranded. */
.inline-form {
  align-items: flex-end;
  row-gap: 1rem;
}
.inline-form .field { flex: 0 1 auto; min-width: 16rem; }

/* Fields on one row line up row by row, not just at the bottom.

   .inline-form was a flex row with align-items:flex-end, which aligns the
   BOTTOM of each field. Give one field a hint and not the other and their
   inputs stop agreeing: the one with a hint sits a line higher, because
   its box extends further down. Labels of different lengths do the same
   thing from above - "Notification email (where contact form submissions
   are sent)" wraps to two lines where "Contact person" takes one.

   A grid with subgrid puts label, control and hint on shared rows, so
   every label starts at the same height, every input sits on the same
   line, and hints line up underneath - whether or not a given field has
   one. */
.inline-form {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  grid-template-rows: auto auto auto auto;
  gap: 0.4rem 1.25rem;
  align-items: start;
}
.inline-form .field {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: 1 / 4;
  gap: 0.4rem;
  min-width: 0;
  margin-bottom: 0;
}
.inline-form > button,
.inline-form > .button,
.inline-form > input[type="submit"] {
  grid-row: 4;
  grid-column: 1 / -1;
  justify-self: start;
  margin-top: 0.75rem;
}

/* Without subgrid the rows can't be shared, so fall back to top alignment
   and a reserved label height - inputs still line up, hints just sit
   wherever their own field ends. */
@supports not (grid-template-rows: subgrid) {
  .inline-form { display: flex; flex-wrap: wrap; align-items: flex-start; }
  .inline-form .field { flex: 1 1 16rem; }
  .inline-form .field > label { min-height: 2.8em; display: flex; align-items: flex-end; }
}

/* Once auto-fit collapses to a single column the row-pinning above turns
   toxic: every field claims rows 1-3, so the second field gets shoved into
   an implicit column hanging past the card's right edge (measured +22px at
   768px). Below one-column width, stop sharing rows and just stack. */
@media (max-width: 900px) {
  .inline-form { display: flex; flex-direction: column; align-items: stretch; }
  .inline-form .field { display: flex; flex-direction: column; }
  .inline-form > button,
  .inline-form > .button,
  .inline-form > input[type="submit"] { align-self: flex-start; }
}

/* ---- Upload guard tooltip ---------------------------------------------
   "You didn't choose a file" belongs beside the button that was pressed,
   not in a flash message at the top of the page where it is often scrolled
   out of view. Positioned above the button and pointing down at it, so the
   connection between message and control is unambiguous. */
.upload-tip-anchor {
  position: relative;
  display: inline-block;
}
.upload-tip {
  position: absolute;
  bottom: calc(100% + 0.5rem);
  left: 0;
  z-index: 20;
  padding: 0.4rem 0.7rem;
  border: 1px solid var(--sm-warning);
  border-radius: 999px;
  background: var(--sm-warning-bg);
  color: var(--sm-warning);
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  pointer-events: none;
  animation: upload-tip-in 0.15s ease-out;
}
/* The little downward notch tying the bubble to its button. */
.upload-tip::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 1.1rem;
  border: 5px solid transparent;
  border-top-color: var(--sm-warning);
}
@keyframes upload-tip-in {
  from { opacity: 0; transform: translateY(3px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .upload-tip { animation: none; }
}

/* ---------- Client detail submenu (tabs) ----------
   The long client page is split into tabbed panels; one shows at a time.
   Styled from the same tokens as the rest of the admin. */
/* The client page's sections, as a submenu beside the content rather than
   a bar across the top - the same shape as the Inbox settings screen
   (.aw-settings / .aw-settings-nav), so the two halves of the product
   navigate the same way. Sticky, so the section list stays put while a
   long panel scrolls.

   The buttons keep their .client-tab class and ARIA roles: admin.js
   queries them by class and manages aria-selected, so this is purely a
   layout change and the keyboard/tab behaviour is untouched. */
.client-layout {
  display: flex;
  gap: 1.25rem;
  align-items: flex-start;
}
.client-tabs {
  flex-shrink: 0;
  width: 13rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  position: sticky;
  top: 1rem;
  padding: 0.625rem;
  background: var(--sm-panel);
  border: 1px solid var(--sm-panel-border);
  border-radius: var(--sm-panel-radius);
  box-shadow: var(--sm-shadow-panel);
  box-sizing: border-box;
}
.client-panels {
  flex: 1;
  min-width: 0;
}
.client-tab {
  font: inherit;
  font-weight: 500;
  font-size: 0.9375rem;
  text-align: left;
  padding: 0.625rem 0.875rem;
  border: 1px solid transparent;
  border-radius: var(--sm-radius);
  background: transparent;
  color: var(--sm-foreground);
  cursor: pointer;
}
.client-tab:hover { background: var(--sm-secondary); }
.client-tab[aria-selected="true"] {
  background: var(--sm-primary);
  color: var(--sm-primary-foreground);
  font-weight: 600;
}
.client-tab:focus-visible { outline: 2px solid var(--sm-ring); outline-offset: 2px; }

/* Narrow screens: the submenu becomes a wrapping row above the content,
   the same fallback the settings screen uses - a 13rem column plus a panel
   does not fit a phone. */
@media (max-width: 60rem) {
  .client-layout { flex-direction: column; }
  .client-tabs {
    width: 100%;
    flex-direction: row;
    flex-wrap: wrap;
    position: static;
  }
  .client-panels { width: 100%; }
}
/* Cards inside a tab panel had NO spacing rule at all: the only one that
   existed is `.app-main > section.card`, a direct-child selector, and every
   card on the client page sits two levels deeper. So every tab holding more
   than one panel had them butting together - which the style guide forbids
   outright ("a minimum 20px gap between any two panels, always. They never
   touch, on desktop or mobile").

   A flex column with a gap rather than margins on the cards: it cannot
   collapse, it does not leave a stray margin under the last card, and it
   applies however the panels are nested. */
.client-tab-panel {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}
.client-tab-panel[hidden] { display: none; }

/* A form may legitimately wrap several panels - one save for three related
   groups of settings - and then IT is the flex child, so the panels inside
   it need the same treatment or they touch again. */
.panel-group {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

/* Belongs with client_detail.html's own synchronous inline script (see its
   comment) - the moment that script sets data-active-tab, on-page CSS wins
   over the server's default `hidden` attributes instantly, before admin.js
   itself even runs. Without this, restoring a remembered tab after a
   full-page reload (e.g. any Widgets & Chat toggle) flashed Overview first:
   deferred JS is fast, but not fast enough to beat first paint. */
.client-layout[data-active-tab] .client-tab-panel { display: none; }
.client-layout[data-active-tab="overview"] #tab-overview,
.client-layout[data-active-tab="widgets"] #tab-widgets,
.client-layout[data-active-tab="contact"] #tab-contact,
.client-layout[data-active-tab="blog"] #tab-blog,
.client-layout[data-active-tab="files"] #tab-files,
.client-layout[data-active-tab="messages"] #tab-messages,
.client-layout[data-active-tab="backups"] #tab-backups,
.client-layout[data-active-tab="actions"] #tab-actions {
  /* flex, not block: this rule wins over .client-tab-panel above, so
     `block` here would quietly switch the gap back off. */
  display: flex;
}

/* ---------- Portability audit tabs ----------
   A horizontal strip inside a card, deliberately NOT the .client-tabs
   side submenu: this sits within a panel that already has its own
   heading, and a second vertical column inside a card reads as a nested
   app rather than a set of sections. Same tokens, different shape.

   With JavaScript off no panel carries the JS-set state, so every panel
   stays visible and the page degrades to the single long list it used to
   be - findings are never hidden by a script failing to run. */
.audit-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin: 0.75rem 0 0;
  border-bottom: 1px solid var(--sm-panel-border);
}
.audit-tab {
  font: inherit;
  font-weight: 500;
  font-size: 0.9375rem;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.875rem;
  border: 1px solid transparent;
  border-bottom: none;
  border-radius: var(--sm-radius) var(--sm-radius) 0 0;
  background: transparent;
  color: var(--sm-foreground);
  cursor: pointer;
  /* Sits on the strip's own 1px border so the selected tab joins its
     panel rather than floating above the line. */
  margin-bottom: -1px;
}
.audit-tab:hover { background: var(--sm-secondary); }
.audit-tab[aria-selected="true"] {
  background: var(--sm-panel);
  border-color: var(--sm-panel-border);
  border-bottom: 1px solid var(--sm-panel);
  font-weight: 600;
}
.audit-tab:focus-visible { outline: 2px solid var(--sm-ring); outline-offset: 2px; }

/* The count rides in the tab itself, so the one number that matters -
   how many real outside connections - is readable without opening
   anything. Neutral by default; only a non-zero count on a tab that
   represents a problem turns red. */
.audit-count {
  font-size: 0.8125rem;
  font-weight: 600;
  min-width: 1.5rem;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  text-align: center;
  background: var(--sm-secondary);
  color: var(--sm-muted-foreground);
}
.audit-count--bad {
  background: var(--sm-destructive, #b91c1c);
  color: #fff;
}
.audit-tab-panel { padding-top: 1rem; }
.audit-tab-panel[hidden] { display: none; }

/* ---------- Portal message thread ----------
   Used by both sides of the same conversation: the client's own portal and
   the Messages tab of the admin client page. The classes say in/out rather
   than client/operator, and each template decides which side it is on.

   These live in THIS stylesheet, not app/static/css/main.css - that one is
   the chat UI's, loaded only by chat_base/layout.html. Everything the
   dashboard and the client portal render comes through base/layout.html,
   which loads this file. */
.msg-thread {
  list-style: none;
  margin: 0 0 1.5rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}
/* Not chat bubbles: these are messages with subjects, often several
   paragraphs, sometimes carrying a document. A bubble makes a 400-word
   update look like an SMS. Cards in a list, with the side shown by which
   edge the accent sits on. */
.msg {
  padding: 1rem 1.25rem;
  border: 1px solid var(--sm-panel-border);
  border-radius: var(--sm-panel-radius);
  background: var(--sm-panel);
  box-shadow: var(--sm-shadow-panel);
}
.msg--in { border-left: 3px solid var(--sm-primary); }
.msg--out { border-right: 3px solid var(--sm-border); }
.msg-meta {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0 0 0.35rem;
  font-size: 0.8125rem;
  color: var(--sm-muted-foreground);
}
.msg-meta strong { color: var(--sm-foreground); font-weight: 600; }
/* The subject is the line you scan a thread by, so it is sized like a real
   heading rather than inheriting whatever h2/h3 happens to mean inside a
   card. */
.msg-subject {
  margin: 0 0 0.5rem;
  font-size: 1.0625rem;
  line-height: 1.3;
  font-weight: 600;
}
.msg-body {
  margin: 0;
  line-height: 1.6;
  overflow-wrap: anywhere;
  max-width: 65ch;
}
.msg-withdrawn { margin: 0; color: var(--sm-muted-foreground); font-style: italic; }
.msg-withdraw { margin: 0.75rem 0 0; }

/* An attached report is somebody else's whole HTML document. Framed, given
   a real height and its own scrollbar so a long one doesn't turn the thread
   into an endless page, with "open full screen" underneath for when the
   frame is too small to read in. */
.msg-report { margin: 0.875rem 0 0; }
.msg-report iframe {
  width: 100%;
  height: 460px;
  border: 1px solid var(--sm-panel-border);
  border-radius: var(--sm-radius);
  background: var(--sm-background);
}

/* A document inside a message: the statement being agreed to, and either
   the form to agree or the record that somebody did. Boxed and tinted so it
   reads as a thing to act on rather than more message text - it is the one
   element in a thread that asks something of the reader. */
.msg-doc {
  margin: 0.875rem 0 0;
  padding: 1rem 1.125rem;
  border: 1px solid var(--sm-primary);
  border-radius: var(--sm-radius);
  background: var(--sm-secondary);
}
.msg-doc--signed { border-color: var(--sm-panel-border); background: transparent; }
.msg-doc-label {
  margin: 0 0 0.5rem;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
/* Quoted rather than run into the page: what it says has to stay legible as
   its own artefact, which is the whole reason the wording is stored per
   message. */
.msg-doc-statement {
  margin: 0 0 1rem;
  padding-left: 0.875rem;
  border-left: 3px solid var(--sm-border);
  font-style: italic;
  line-height: 1.6;
  max-width: 65ch;
}
.msg-doc-form { max-width: 26rem; }
.msg-doc-agree { display: flex; align-items: center; gap: 0.5rem; }
.msg-doc-agree input { width: auto; }

/* The unreleased site, framed inside the portal. Tall enough to judge a
   homepage by, with "open in a new tab" underneath - a frame is never a
   fair test of a full-width design. */
.preview-frame {
  border: 1px solid var(--sm-panel-border);
  border-radius: var(--sm-panel-radius);
  overflow: hidden;
  background: var(--sm-background);
  box-shadow: var(--sm-shadow-panel);
}
.preview-frame iframe { display: block; width: 100%; height: 70vh; min-height: 24rem; border: 0; }

/* A card that asks for something rather than telling you something. One
   accent edge and nothing else - the attention comes from the border and
   the heading, not a colour wash that would fight the page. */
.card--attention { border-left: 3px solid var(--sm-primary); }

/* A table-row action. Full-height buttons make every row taller, and a list
   of forty clients pays for that forty times. */
.button--small { padding: 0.35rem 0.7rem; font-size: 0.8125rem; }

/* The operator is signed in as somebody else. Deliberately the loudest
   thing on the page and pinned above everything, because the failure mode
   is not a bug - it is forgetting whose screen this is and typing as them. */
.impersonation-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1.5rem;
  padding: 0.75rem 1.25rem;
  background: var(--sm-primary);
  color: var(--sm-primary-foreground);
  font-size: 0.9375rem;
  line-height: 1.45;
}
.impersonation-bar strong { font-weight: 700; }
.impersonation-bar form { margin: 0; }
/* The way out never inherits the page's own button colours - it has to stay
   readable sitting on the accent. */
.impersonation-bar .button {
  background: var(--sm-foreground);
  color: var(--sm-background);
  border-color: var(--sm-foreground);
}
.impersonation-bar .button:hover { opacity: 0.9; background: var(--sm-foreground); }
