/* Built by design-system/sync.sh from:
     00-fonts.css
     01-tokens.css
     themes/portal.css
     02-base.css
     03-components.css
     06-app.css
   Edit the sources, not this file. */

/* =========================================================
   Lunolyte Design System: Fonts
   ---------------------------------------------------------
   GENERATED FILE. Do not edit inside an app.
   Source: design-system/css/00-fonts.css
   ---------------------------------------------------------
   Geist and Geist Mono, self-hosted. Every app runs CSP
   font-src 'self', so a font CDN is not reachable, and a
   monitoring product whose pages stall on a third-party host
   is not a good look regardless.

   The url() is relative to this stylesheet, not to the page,
   so the same declaration works whether an app serves its
   static files from /public or from the root.

   If the woff2 files are absent these rules simply fail and
   every app falls back to the platform UI face. Nothing
   breaks, the type just looks like the system default.
   ========================================================= */

@font-face {
  font-family: 'Geist';
  src: url('fonts/Geist.woff2') format('woff2-variations'),
       url('fonts/Geist.woff2') format('woff2');
  font-weight: 100 900;          /* single variable file covers the range */
  font-style: normal;
  font-display: swap;            /* show fallback text immediately, swap when loaded */
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122,
                 U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Geist Mono';
  src: url('fonts/GeistMono.woff2') format('woff2-variations'),
       url('fonts/GeistMono.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122,
                 U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* =========================================================
   Lunolyte Design System: Core tokens
   ---------------------------------------------------------
   GENERATED FILE. Do not edit inside an app.
   Source: design-system/css/01-tokens.css
   Apply changes there and run design-system/sync.sh
   ---------------------------------------------------------
   Everything here is app-independent. Accent colours and the
   surface palette live in the per-app theme file that loads
   immediately before this one, so this file may reference
   --accent / --bg-* but never defines them.
   ========================================================= */

:root {
  /* ── Type family ──────────────────────────────────────
     Geist for everything, Geist Mono for anything the eye has
     to scan character by character: timestamps, incident ids,
     URLs, status codes. One family keeps the estate coherent;
     the matching mono is why this pair was chosen over Inter.

     The fallbacks are live until the woff2 files are added to
     design-system/assets/fonts. See 00-fonts.css. */
  --font-ui:      'Geist', system-ui, -apple-system, 'Segoe UI Variable Text', 'Segoe UI', sans-serif;
  --font-display: 'Geist', system-ui, -apple-system, 'Segoe UI Variable Display', 'Segoe UI', sans-serif;
  --font-mono:    'Geist Mono', ui-monospace, 'Cascadia Code', 'SF Mono', 'Fira Mono', monospace;

  /* ── Type scale ───────────────────────────────────────
     Ratio-based, 16px base. The two largest are fluid so a
     marketing hero can breathe without a media query.
     Rule: no raw px font-size anywhere downstream. */
  --fs-2xs:  0.6875rem;  /* 11 dense table meta */
  --fs-xs:   0.75rem;    /* 12 pills, captions */
  --fs-sm:   0.8125rem;  /* 13 labels, secondary */
  --fs-md:   0.875rem;   /* 14 default UI body */
  --fs-base: 1rem;       /* 16 prose */
  --fs-lg:   1.125rem;   /* 18 card titles */
  --fs-xl:   1.375rem;   /* 22 page titles */
  --fs-2xl:  1.75rem;    /* 28 stat values */
  --fs-3xl:  clamp(2rem,   1.40rem + 2.6vw, 3rem);
  --fs-4xl:  clamp(2.5rem, 1.50rem + 4.5vw, 4.5rem);

  --fw-regular:  400;
  --fw-medium:   500;
  --fw-semibold: 600;
  --fw-bold:     700;

  --lh-tight:   1.15;
  --lh-snug:    1.30;
  --lh-normal:  1.50;
  --lh-relaxed: 1.65;

  --ls-tight:  -0.022em;   /* large display text only */
  --ls-snug:   -0.011em;
  --ls-normal:  0;
  --ls-wide:    0.06em;    /* uppercase eyebrows */

  /* ── Space ────────────────────────────────────────────
     4px grid. These existed before but were never used;
     downstream files must use them instead of raw px. */
  --space-3xs: 2px;
  --space-2xs: 4px;
  --space-xs:  8px;
  --space-sm:  12px;
  --space-md:  16px;
  --space-lg:  24px;
  --space-xl:  40px;
  --space-2xl: 64px;
  --space-3xl: 96px;

  /* ── Radius ───────────────────────────────────────────── */
  --radius-sm:   6px;
  --radius-md:   10px;
  --radius-lg:   14px;
  --radius-xl:   20px;
  --radius-full: 999px;

  /* ── Elevation ────────────────────────────────────────── */
  --shadow-soft:  0 1px 3px rgba(0,0,0,0.45), 0 1px 2px rgba(0,0,0,0.30);
  --shadow-card:  0 4px 24px rgba(0,0,0,0.55), 0 1px 6px rgba(0,0,0,0.40);
  --shadow-lift:  0 12px 40px rgba(0,0,0,0.55);
  --shadow-modal: 0 24px 64px rgba(0,0,0,0.65);

  /* ── Layering ─────────────────────────────────────────
     Named so nothing downstream invents a z-index. */
  --z-base:     0;
  --z-raised:   10;
  --z-sticky:   100;
  --z-sidebar:  200;
  --z-dropdown: 300;
  --z-modal:    400;
  --z-toast:    500;

  /* ── Motion ───────────────────────────────────────────── */
  --dur-fast: 120ms;
  --dur-med:  200ms;
  --dur-slow: 320ms;
  --ease-out:    cubic-bezier(0.20, 0.80, 0.20, 1);
  --ease-in-out: cubic-bezier(0.40, 0.00, 0.20, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);

  /* ── Layout ───────────────────────────────────────────── */
  --sidebar-w:   240px;
  --topbar-h:    60px;
  --wrap-max:    1180px;
  --wrap-narrow: 760px;
  --wrap-prose:  680px;

  /* ── Status colours ───────────────────────────────────
     Identical across every app so "operational" never means
     two different greens. Each is a fill/ink pair. */
  --ok-fill:      rgba(34,197,94,0.12);
  --ok-line:      rgba(34,197,94,0.28);
  --ok-ink:       #4ADE80;
  --warn-fill:    rgba(249,115,22,0.12);
  --warn-line:    rgba(249,115,22,0.30);
  --warn-ink:     #FB923C;
  --bad-fill:     rgba(239,68,68,0.12);
  --bad-line:     rgba(239,68,68,0.30);
  --bad-ink:      #F87171;
  --info-fill:    rgba(168,85,247,0.12);
  --info-line:    rgba(168,85,247,0.30);
  --info-ink:     #C084FC;
  --neutral-fill: rgba(148,163,184,0.10);
  --neutral-line: rgba(148,163,184,0.22);
  --neutral-ink:  #94A3B8;
}

/* =========================================================
   Lunolyte theme: Customer Portal
   ---------------------------------------------------------
   GENERATED FILE. Do not edit inside an app.
   Source: design-system/css/themes/portal.css
   ---------------------------------------------------------
   Same surfaces as every other app; only the accent differs,
   so a customer can tell portal from admin at a glance without
   the two feeling like different products.

   Dark only. The portals have never had a light mode and
   adding one is deliberately out of scope for the overhaul.
   ========================================================= */

:root {
  color-scheme: dark;

  --bg-page:        #060911;
  --bg-shell:       #0A0F1A;
  --bg-card:        #0E1524;
  --bg-card-strong: #141D30;
  --bg-input:       #080D18;

  --text-main:  #E8EEF6;
  --text-muted: #8FA3BC;
  --text-soft:  #556882;

  --accent:        #38BDF8;
  --accent-strong: #7DD3FC;
  --accent-ink:    #04202F;
  --accent-soft:   rgba(56,189,248,0.10);
  --accent-glow:   rgba(56,189,248,0.28);

  --border-soft:   rgba(148,196,220,0.10);
  --border-strong: rgba(148,196,220,0.22);

  --focus: var(--accent-strong);

  --glow-1: rgba(56,189,248,0.15);
  --glow-2: rgba(99,102,241,0.12);
}

/* =========================================================
   Lunolyte Design System: Reset + base typography
   ---------------------------------------------------------
   GENERATED FILE. Do not edit inside an app.
   Source: design-system/css/02-base.css
   ========================================================= */

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

html {
  /* 16px base. The type scale is rem-based, so this is the one
     place a global size change would ripple from. */
  font-size: 100%;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg-page);
  color: var(--text-main);
  font-family: var(--font-ui);
  font-size: var(--fs-md);
  line-height: var(--lh-normal);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-synthesis-weight: none;
}

h1, h2, h3, h4, h5, h6 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-snug);
  color: var(--text-main);
  text-wrap: balance;
}

p { margin: 0; }

/* Long-form copy reads better with a wider measure than UI text. */
p, li { text-wrap: pretty; }

a {
  color: var(--accent);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}
a:hover { color: var(--accent-strong); }

img, svg, video { max-width: 100%; }
img, video { height: auto; display: block; }

code, kbd, pre, samp { font-family: var(--font-mono); font-size: 0.925em; }

button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }

table { border-collapse: collapse; width: 100%; }

hr {
  border: 0;
  border-top: 1px solid var(--border-soft);
  margin: var(--space-lg) 0;
}

/* ── Focus ────────────────────────────────────────────────
   One ring, everywhere. Before the overhaul only inputs had a
   focus style, which left keyboard users with no idea where
   they were on any page in the estate. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
:focus:not(:focus-visible) { outline: none; }

/* ── Motion ───────────────────────────────────────────────
   Covers every animation and transition in every downstream
   file at once, including any added later. */
@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;
  }
}

/* ── Utilities ───────────────────────────────────────────
   Deliberately few. This is not a utility framework; anything
   used more than a handful of times becomes a component. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.wrap {
  width: 100%;
  max-width: var(--wrap-max);
  margin-inline: auto;
  padding-inline: var(--space-lg);
}
.wrap-narrow { max-width: var(--wrap-narrow); }
.wrap-prose  { max-width: var(--wrap-prose); }

.text-muted { color: var(--text-muted); }
.text-soft  { color: var(--text-soft); }
.text-mono  { font-family: var(--font-mono); }

/* Horizontal scroll containers: wide tables and code must
   scroll inside themselves, never make the page scroll. */
.scroll-x { overflow-x: auto; max-width: 100%; }

/* =========================================================
   Lunolyte Design System: Shared components
   ---------------------------------------------------------
   GENERATED FILE. Do not edit inside an app.
   Source: design-system/css/03-components.css
   ---------------------------------------------------------
   Every app gets these verbatim. App-specific layout (sidebar,
   status page chrome, marketing hero) lives in that app's own
   layer, which loads after this one.
   ========================================================= */

/* ── Wordmark ─────────────────────────────────────────────
   The shimmer sweeps twice on load and then stops. An
   infinitely shimmering logo reads as broken within a week and
   burns GPU on every page in the estate.

   The solid colour is declared OUTSIDE the animation so a
   reduced-motion visitor, or any browser without
   background-clip:text, still sees a legible wordmark rather
   than transparent nothing. */
.wordmark {
  font-family: var(--font-display);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-tight);
  color: var(--text-main);
}

@supports (background-clip: text) or (-webkit-background-clip: text) {
  @media (prefers-reduced-motion: no-preference) {
    .wordmark {
      background: linear-gradient(
        100deg,
        var(--text-main) 0%,
        var(--text-main) 38%,
        var(--accent-strong) 50%,
        var(--text-main) 62%,
        var(--text-main) 100%
      );
      background-size: 220% 100%;
      background-position: 120% 0;
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;
      animation: wordmark-shimmer 2.6s var(--ease-in-out) 2;
    }
  }
}

@keyframes wordmark-shimmer {
  from { background-position: 120% 0; }
  to   { background-position: -40% 0; }
}

/* ── Brand mark ───────────────────────────────────────────── */
.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  color: var(--text-main);
}
.brand-mark {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);
  background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%);
  color: var(--accent-ink);
  font-family: var(--font-display);
  font-weight: var(--fw-bold);
  font-size: var(--fs-md);
  letter-spacing: 0;
}
.brand-text { display: flex; flex-direction: column; line-height: 1.1; }
.brand-name { font-weight: var(--fw-semibold); font-size: var(--fs-md); }
.brand-tag {
  font-size: var(--fs-2xs);
  color: var(--text-soft);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
}

/* ── Buttons ──────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  height: 40px;
  padding-inline: var(--space-md);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--text-main);
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  line-height: 1;
  white-space: nowrap;
  transition:
    background var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn[disabled], .btn[aria-disabled="true"] {
  opacity: 0.5;
  pointer-events: none;
  transform: none;
}
.btn svg { width: 16px; height: 16px; flex-shrink: 0; }

.btn-primary {
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: var(--fw-semibold);
}
.btn-primary:hover { background: var(--accent-strong); color: var(--accent-ink); }

.btn-secondary {
  background: var(--bg-card-strong);
  border-color: var(--border-strong);
  color: var(--text-main);
}
.btn-secondary:hover { background: var(--bg-card); border-color: var(--accent); color: var(--text-main); }

.btn-ghost { color: var(--text-muted); }
.btn-ghost:hover { background: var(--bg-card); color: var(--text-main); }

.btn-danger { background: var(--bad-ink); color: #2A0A0A; font-weight: var(--fw-semibold); }
.btn-danger:hover { background: #FCA5A5; color: #2A0A0A; }

.btn-sm { height: 32px; padding-inline: var(--space-sm); font-size: var(--fs-sm); }
.btn-lg { height: 48px; padding-inline: var(--space-lg); font-size: var(--fs-base); }
.btn-full { width: 100%; }

/* ── Cards ────────────────────────────────────────────────── */
/* A bare .card carries its own padding. Most cards in the portals hold
   content directly rather than a .card-body, and without this their text
   sat flush against the border. The :has() rules drop it again when the
   card uses the structured head/body/foot parts, which bring their own,
   or when it wraps a table that must reach the card's edge.

   Where :has() is unsupported those cards simply get roomier, never
   broken, which is the right way round for a progressive enhancement. */
.card {
  padding: var(--space-lg);
  background: var(--bg-card);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}
/* Only the structured parts cancel the padding, because they bring their
   own. A card holding a table keeps its padding: these cards usually carry
   a heading too, and zeroing the padding for the table's sake pushed that
   heading flat against the border. */
.card:has(> .card-head),
.card:has(> .card-body),
.card:has(> .card-foot) { padding: 0; }

/* A card can still put a bare heading next to a structured body. The body
   pads itself, the card no longer pads at all, and the heading was left
   sitting against the border. It gets the same inset here, and the body
   below it tightens up so the two do not stack two full gaps apart. */
.card:has(> .card-body) > h1,
.card:has(> .card-body) > h2,
.card:has(> .card-body) > h3,
.card:has(> .card-body) > .section-title {
  margin: 0;
  padding: var(--space-lg) var(--space-lg) 0;
}
.card > h1 + .card-body,
.card > h2 + .card-body,
.card > h3 + .card-body,
.card > .section-title + .card-body { padding-top: var(--space-sm); }

/* Stacked sections need air between them, but only where the container
   actually stacks them: a grid or flex parent already spaces its own children
   with gap, and a second gap on top of that lands on every child but the
   first, knocking a row out of line.

   CSS cannot ask a parent what layout mode it is in, so a container that
   stacks its children in normal flow says so by carrying .flow. Expressing it
   the other way round, by naming the grids to exclude, means every grid that
   is ever added has to be remembered; the marketing feature grid was not, and
   every card in it but the first picked up a stray margin. */
.flow > * + * { margin-top: var(--space-lg); }

/* A section title introduces the block underneath it rather than standing
   apart from it, so it keeps the tighter gap a section header sets. */
.flow > .section-title + * { margin-top: var(--space-sm); }
.card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--border-soft);
}
.card-title { font-size: var(--fs-lg); font-weight: var(--fw-semibold); }
.card-sub { font-size: var(--fs-sm); color: var(--text-muted); margin-top: var(--space-3xs); }
.card-body { padding: var(--space-lg); }
.card-foot {
  padding: var(--space-md) var(--space-lg);
  border-top: 1px solid var(--border-soft);
  display: flex;
  gap: var(--space-xs);
  justify-content: flex-end;
}

/* Interactive card: only for cards that are genuinely a link. */
.card-link { transition: border-color var(--dur-med) var(--ease-out), transform var(--dur-med) var(--ease-out); }
.card-link:hover { border-color: var(--border-strong); transform: translateY(-2px); }

/* The anchored tier in a pricing set. Contrast effect: this only
   works if the surrounding options are visibly plainer. */
.card-featured {
  border-color: var(--accent);
  box-shadow: var(--shadow-lift), 0 0 0 1px var(--accent-soft);
}

/* ── Pills and badges ─────────────────────────────────────
   Tone is a closed vocabulary. A database value that maps to
   nothing falls back to neutral, so an unknown status renders
   as a plain pill instead of unstyled text. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: 3px var(--space-xs);
  border: 1px solid transparent;
  border-radius: var(--radius-full);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  line-height: 1.4;
  white-space: nowrap;
}
.tone-ok      { background: var(--ok-fill);      border-color: var(--ok-line);      color: var(--ok-ink); }
.tone-warn    { background: var(--warn-fill);    border-color: var(--warn-line);    color: var(--warn-ink); }
.tone-bad     { background: var(--bad-fill);     border-color: var(--bad-line);     color: var(--bad-ink); }
.tone-info    { background: var(--info-fill);    border-color: var(--info-line);    color: var(--info-ink); }
.tone-neutral { background: var(--neutral-fill); border-color: var(--neutral-line); color: var(--neutral-ink); }
.tone-accent  { background: var(--accent-soft);  border-color: var(--border-strong); color: var(--accent); }

.dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}
/* Only a live "something is wrong" state pulses, and it stops
   under reduced motion via the global rule in 02-base. */
.dot-live { animation: dot-pulse 2.4s var(--ease-in-out) infinite; }
@keyframes dot-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}

/* ── Eyebrow ──────────────────────────────────────────────── */
.eyebrow {
  display: inline-block;
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
  color: var(--accent);
}

/* ── Forms ────────────────────────────────────────────────── */
.field { display: flex; flex-direction: column; gap: var(--space-2xs); }
.field + .field { margin-top: var(--space-md); }
.field-label { font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--text-muted); }
.field-hint  { font-size: var(--fs-xs); color: var(--text-soft); }
.field-error { font-size: var(--fs-xs); color: var(--bad-ink); }

.input {
  width: 100%;
  height: 40px;
  padding-inline: var(--space-sm);
  background: var(--bg-input);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  color: var(--text-main);
  font-size: var(--fs-md);
  transition: border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
.input::placeholder { color: var(--text-soft); }
.input:hover { border-color: var(--border-strong); }
.input:focus { border-color: var(--accent); outline: none; }
.input:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
textarea.input { height: auto; min-height: 96px; padding-block: var(--space-xs); resize: vertical; }

select.input {
  appearance: none;
  padding-right: var(--space-xl);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238FA3BC' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-sm) center;
  background-size: 16px;
  cursor: pointer;
}
/* The list itself is drawn by the OS and cannot be styled, but the options
   inherit these, which stops a dark page from dropping open a white menu. */
select.input option { background: var(--bg-card); color: var(--text-main); }

/* Date and time inputs render a native picker button that defaults to a
   dark glyph, invisible against a dark field. invert flips it, and the
   whole control opens the picker rather than just that small target. */
.input[type="date"],
.input[type="time"],
.input[type="datetime-local"] { cursor: pointer; }
.input[type="date"]::-webkit-calendar-picker-indicator,
.input[type="time"]::-webkit-calendar-picker-indicator,
.input[type="datetime-local"]::-webkit-calendar-picker-indicator {
  filter: invert(0.7);
  cursor: pointer;
  opacity: 0.8;
}
.input[type="date"]::-webkit-calendar-picker-indicator:hover,
.input[type="time"]::-webkit-calendar-picker-indicator:hover,
.input[type="datetime-local"]::-webkit-calendar-picker-indicator:hover { opacity: 1; }

/* ── Empty state ──────────────────────────────────────────── */
.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-2xl) var(--space-lg);
  text-align: center;
  color: var(--text-muted);
}
.empty-title { font-size: var(--fs-base); font-weight: var(--fw-semibold); color: var(--text-main); }
.empty-desc  { font-size: var(--fs-sm); max-width: 42ch; }

/* ── Banner ───────────────────────────────────────────────── */
.banner {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  font-size: var(--fs-md);
}
.banner-title { font-weight: var(--fw-semibold); }
.banner-body  { font-size: var(--fs-sm); opacity: 0.85; }

/* ── Signup ───────────────────────────────────────────────── */
/* A wider auth card, because picking a plan needs room that signing in does
   not. Everything else about it is the same card. */
.auth-card-wide { max-width: 34rem; }

.auth-foot {
  margin-top: var(--space-md);
  font-size: var(--fs-sm);
  color: var(--text-muted);
  text-align: center;
}

/* Monthly against yearly. Radios, not links: the first version navigated to
   change the interval, which reloaded the page and threw away the company
   name and email the visitor had already typed. */
.seg {
  display: flex;
  gap: var(--space-3xs);
  padding: var(--space-3xs);
  background: var(--bg-input);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
}
.seg-option {
  flex: 1;
  padding: var(--space-2xs) var(--space-xs);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  color: var(--text-muted);
  text-align: center;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.seg-option { cursor: pointer; }
.seg-option:hover { color: var(--text-main); }
.seg-option:has(input:checked) {
  background: var(--bg-card-strong);
  color: var(--text-main);
  font-weight: var(--fw-semibold);
}
.seg-option:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Both prices are in the page and the chosen interval decides which is shown,
   so switching costs no request and loses nothing that was typed. */
.signup-form .amount-yearly,
.signup-form:has(#interval-yearly:checked) .amount-monthly { display: none; }
.signup-form:has(#interval-yearly:checked) .amount-yearly  { display: inline; }

.plan-options {
  display: grid;
  gap: var(--space-2xs);
  margin-block: var(--space-md);
}

/* The radio keeps the keyboard and the accessibility tree; the label is what
   gets drawn. :has reads the checked state, so which plan is selected has one
   source of truth instead of a class the server also has to guess at. */
.plan-option {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--space-3xs) var(--space-xs);
  padding: var(--space-sm) var(--space-md);
  background: var(--bg-card);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
.plan-option:hover { border-color: var(--border-strong); }
.plan-option:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.plan-option:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.plan-option input,
.seg-option input {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.plan-option-name  { font-weight: var(--fw-semibold); color: var(--text-main); }
.plan-option-price { font-weight: var(--fw-semibold); color: var(--text-main); text-align: right; }
.plan-option-per   { font-size: var(--fs-xs); font-weight: var(--fw-regular); color: var(--text-soft); }
.plan-option-meta  { grid-column: 1 / -1; font-size: var(--fs-xs); color: var(--text-soft); }

/* ── Section rhythm ───────────────────────────────────────── */
.section { padding-block: var(--space-3xl); }
.section-alt {
  background: var(--bg-shell);
  border-block: 1px solid var(--border-soft);
}
/* The measure is set on the text elements themselves, never on this
   wrapper. `ch` resolves against the element's OWN font-size, so a
   max-width here would be computed at body size (14px) and then applied
   to a 48px heading, squeezing a two-line headline into three or four. */
.section-head { margin-bottom: var(--space-xl); }
.section-head h2 {
  max-width: 22ch;
  font-size: var(--fs-3xl);
  letter-spacing: var(--ls-tight);
  margin-top: var(--space-sm);
}
.section-head .lede {
  max-width: 60ch;
  margin-top: var(--space-sm);
  font-size: var(--fs-base);
  color: var(--text-muted);
  line-height: var(--lh-relaxed);
}
.section-head-center { text-align: center; }
.section-head-center h2,
.section-head-center .lede { margin-inline: auto; }

/* ── Auth screens ─────────────────────────────────────────
   Shared by all three signed-out surfaces: both portals and the private
   status page. It lived in the portal layer, which the status app does
   not load, so that login screen rendered unstyled. */
.auth-wrap { min-height: 100vh; display: grid; place-items: center; padding: var(--space-lg); }
.auth-card {
  position: relative;
  width: 100%;
  max-width: 400px;
  padding: var(--space-xl);
  background: var(--bg-card);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}
.auth-wrap::before {
  content: '';
  position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background: radial-gradient(40rem 30rem at 50% 0%, var(--glow-1), transparent 65%);
}
.auth-title { margin-top: var(--space-md); font-size: var(--fs-xl); }
.auth-subtitle { margin-top: var(--space-xs); font-size: var(--fs-sm); color: var(--text-muted); line-height: var(--lh-relaxed); }
.auth-form { margin-top: var(--space-lg); }
.auth-card .btn-full { margin-top: var(--space-lg); }
.auth-icon {
  display: grid;
  place-items: center;
  width: 44px; height: 44px;
  margin-top: var(--space-lg);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
}
.auth-icon svg { width: 22px; height: 22px; }

.auth-hint { margin-top: var(--space-md); font-size: var(--fs-xs); color: var(--text-soft); }
body.auth-page { display: grid; place-items: stretch; }

/* ── Grid ─────────────────────────────────────────────────── */
.grid { display: grid; gap: var(--space-md); }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

@media (max-width: 900px) {
  .grid-3, .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 620px) {
  .grid-2, .grid-3, .grid-4 { grid-template-columns: minmax(0, 1fr); }
}

/* =========================================================
   Lunolyte Design System: Portal shell
   ---------------------------------------------------------
   GENERATED FILE. Do not edit inside an app.
   Source: design-system/css/06-app.css
   ---------------------------------------------------------
   Shared by BOTH portals. They previously ran two stylesheets
   that were 79% byte-identical yet had drifted apart in the
   details, which is the whole reason this file exists. The
   only difference between admin and customer portal is the
   accent, which comes from the theme file.

   No scroll choreography here. Motion is functional only:
   focus, hover, open and close. A screen full of tables is
   not the place for a page to perform.
   ========================================================= */

body.app { min-height: 100vh; }

/* ── Shell ────────────────────────────────────────────────── */
.sidebar {
  position: fixed;
  top: 0; left: 0; bottom: 0;
  z-index: var(--z-sidebar);
  width: var(--sidebar-w);
  display: flex;
  flex-direction: column;
  background: var(--bg-shell);
  border-right: 1px solid var(--border-soft);
}
.app-main {
  margin-left: var(--sidebar-w);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.page-body { flex: 1; padding: var(--space-lg); }
.app-footer {
  padding: var(--space-md) var(--space-lg);
  border-top: 1px solid var(--border-soft);
  font-size: var(--fs-xs);
  color: var(--text-soft);
}

.topbar {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  height: var(--topbar-h);
  padding: 0 var(--space-lg);
  background: color-mix(in srgb, var(--bg-page) 85%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border-soft);
}
.topbar-start { display: flex; align-items: center; gap: var(--space-xs); min-width: 0; }
.topbar-end { display: flex; align-items: center; gap: var(--space-xs); flex-shrink: 0; }
.topbar-ctx {
  font-family: var(--font-display);
  font-weight: var(--fw-semibold);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ── Brand ────────────────────────────────────────────────── */
.sidebar .brand {
  padding: var(--space-md);
  border-bottom: 1px solid var(--border-soft);
}

/* ── Context switcher ─────────────────────────────────────
   The separation between the platform's own data and its
   customers used to be two labelled groups in one long
   sidebar, which put "Incidents" in the menu twice with only
   the heading above it saying which was which.

   A <details> so it works with no JS at all. */
.ctx {
  position: relative;
  z-index: var(--z-dropdown);
  margin: var(--space-sm);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  background: var(--bg-card);
}
.ctx > summary {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-sm);
  cursor: pointer;
  list-style: none;
  border-radius: var(--radius-md);
}
.ctx > summary::-webkit-details-marker { display: none; }
.ctx > summary::after {
  content: '';
  margin-left: auto;
  width: 7px; height: 7px;
  border-right: 1.5px solid var(--text-soft);
  border-bottom: 1.5px solid var(--text-soft);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--dur-fast) var(--ease-out);
}
.ctx[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.ctx:hover { border-color: var(--border-strong); }

.ctx-label { display: block; font-size: var(--fs-2xs); color: var(--text-soft); text-transform: uppercase; letter-spacing: var(--ls-wide); }
.ctx-current { display: block; font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text-main); }

/* The list floats over the navigation instead of pushing it down. Opening a
   two-item menu should not shove every nav link out from under the pointer. */
.ctx-list {
  position: absolute;
  top: calc(100% + var(--space-2xs));
  left: 0;
  right: 0;
  padding: var(--space-2xs);
  background: var(--bg-card-strong);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lift);
}
.ctx-option {
  display: block;
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.ctx-option:hover { background: var(--bg-card-strong); color: var(--text-main); }
.ctx-option[aria-current="true"] { color: var(--accent); font-weight: var(--fw-medium); }

/* ── Nav ──────────────────────────────────────────────────── */
.sidebar-nav { flex: 1; overflow-y: auto; padding: var(--space-xs) var(--space-sm); }
.nav-section-label {
  padding: var(--space-md) var(--space-xs) var(--space-2xs);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
  color: var(--text-soft);
}
.nav-link {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-md);
  color: var(--text-muted);
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.nav-link:hover { background: var(--bg-card); color: var(--text-main); }
.nav-link.active { background: var(--accent-soft); color: var(--accent); }
.nav-icon { display: grid; place-items: center; width: 18px; height: 18px; flex-shrink: 0; }
.nav-icon svg { width: 16px; height: 16px; }

.sidebar-foot { padding: var(--space-sm); border-top: 1px solid var(--border-soft); }
.sidebar-user { display: flex; align-items: center; gap: var(--space-xs); padding: var(--space-xs) var(--space-sm); }
.sidebar-user-avatar {
  display: grid; place-items: center;
  width: 28px; height: 28px; flex-shrink: 0;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
}
.sidebar-user-info { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
.sidebar-user-name { font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-user-role { font-size: var(--fs-2xs); color: var(--text-soft); text-transform: capitalize; }
.nav-link-logout { color: var(--text-soft); }

.sidebar-toggle {
  display: none;
  position: fixed;
  top: var(--space-sm); left: var(--space-sm);
  z-index: var(--z-dropdown);
  width: 36px; height: 36px;
  align-items: center; justify-content: center;
  background: var(--bg-card);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  color: var(--text-muted);
}
.sidebar-scrim {
  display: none;
  position: fixed; inset: 0;
  z-index: calc(var(--z-sidebar) - 1);
  background: rgba(0,0,0,0.5);
}

/* ── Page header ──────────────────────────────────────────── */
.page-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
  margin-bottom: var(--space-lg);
}
.page-title { font-size: var(--fs-xl); letter-spacing: var(--ls-snug); }
.page-subtitle { margin-top: var(--space-3xs); font-size: var(--fs-sm); color: var(--text-muted); }
.page-actions { display: flex; gap: var(--space-xs); flex-wrap: wrap; }

.section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  margin-bottom: var(--space-sm);
}
.section-title { font-size: var(--fs-lg); font-weight: var(--fw-semibold); }

.back-btn { display: inline-flex; align-items: center; gap: var(--space-2xs); font-size: var(--fs-sm); color: var(--text-muted); margin-bottom: var(--space-sm); }
.back-btn:hover { color: var(--text-main); }

/* ── Stats ────────────────────────────────────────────────── */
/* No margin-bottom: whatever follows a stats grid claims that gap itself,
   so a screen cannot end up with two gaps stacked. */
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--space-md); }
.stat-card { padding: var(--space-md); background: var(--bg-card); border: 1px solid var(--border-soft); border-radius: var(--radius-lg); }
.stat-label { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: var(--ls-wide); color: var(--text-soft); }
.stat-value { margin-top: var(--space-2xs); font-family: var(--font-display); font-size: var(--fs-2xl); font-weight: var(--fw-bold); letter-spacing: var(--ls-tight); }
.stat-hint { margin-top: var(--space-3xs); font-size: var(--fs-xs); color: var(--text-muted); }

/* ── Tables ───────────────────────────────────────────────── */
.table-wrap {
  /* Both axes, not just x. overflow-x alone leaves the block direction
     visible, so the header cell's own background paints straight over the
     rounded top corners and they read as square. */
  overflow: auto;
  background: var(--bg-card);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
}
/* A wrap inside a card must not draw a second border and radius on top of
   the card's own. Two rounded rectangles of the same radius, one inset by a
   1px border, leave the inner corner poking past the outer one, which is
   the square-on-top-of-round edge. The card already supplies both. */
.card > .table-wrap,
.card > .scroll-x,
.card-body > .table-wrap,
.card-body > .scroll-x {
  border: 0;
  background: transparent;
  border-radius: var(--radius-md);
}

/* The wrap's overflow does the clipping, so the header cell's square
   background is cut to the wrap's radius without the cells needing a radius
   of their own. Verified: a th computes 0px here and still renders round. */
.data-table { border-radius: inherit; }
.data-table { width: 100%; font-size: var(--fs-md); }
.data-table th {
  padding: var(--space-xs) var(--space-md);
  text-align: left;
  font-size: var(--fs-2xs);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
  color: var(--text-soft);
  background: var(--bg-card-strong);
  border-bottom: 1px solid var(--border-soft);
  white-space: nowrap;
}
.data-table td { padding: var(--space-sm) var(--space-md); border-bottom: 1px solid var(--border-soft); vertical-align: middle; }
.data-table tbody tr:last-child td { border-bottom: 0; }
.data-table tbody tr:hover { background: var(--bg-card-strong); }
.data-table .ta-right { text-align: right; }
.data-table .ta-center { text-align: center; }
.data-table.is-dense td { padding: var(--space-2xs) var(--space-md); }
/* Row actions should not drag the row wider than the viewport. */
.row-actions { display: flex; gap: var(--space-2xs); justify-content: flex-end; white-space: nowrap; }

.table-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  flex-wrap: wrap;
  padding: var(--space-sm) var(--space-md);
  border-bottom: 1px solid var(--border-soft);
  background: var(--bg-card-strong);
}
.table-toolbar .input { height: 34px; font-size: var(--fs-sm); }
.table-toolbar .input[type="search"] { flex: 1; min-width: 160px; }

/* ── Filter bar ───────────────────────────────────────────
   A row, not a stack. Each field sizes to what it holds: a date input
   needs about 9 characters, a name field wants room to grow. Left as
   plain block inputs these ran the full width of the page, one per line,
   and pushed the table itself below the fold. */
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-sm);
  background: var(--bg-card);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
}
/* .table-toolbar is the same row of controls under another name: .filter-bar
   submits a GET, the toolbar filters in the browser, and they are meant to be
   indistinguishable on screen. Named together here rather than written twice,
   which is how the toolbar came to stack its inputs full width long after the
   filter bar was fixed. */
.filter-bar .input,
.table-toolbar .input {
  flex: 1 1 160px;
  min-width: 0;
  max-width: 240px;
  height: 34px;
  font-size: var(--fs-sm);
}
.filter-bar .input[type="date"],
.table-toolbar .input[type="date"] { flex: 0 0 auto; width: 150px; }
.filter-bar .btn,
.table-toolbar .btn { flex-shrink: 0; }
/* Pushes the actions to the end of the row on a wide screen, without
   stranding them on their own line when the fields wrap. */
.filter-bar .filter-actions,
.table-toolbar .filter-actions { display: flex; gap: var(--space-xs); margin-left: auto; }

@media (max-width: 720px) {
  .filter-bar .input,
  .table-toolbar .input { max-width: none; flex-basis: 100%; }
  .filter-bar .filter-actions,
  .table-toolbar .filter-actions { margin-left: 0; }
}

/* ── Pagination ───────────────────────────────────────────── */
.pagination { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); flex-wrap: wrap; margin-top: var(--space-md); }
.pagination-sizes { display: flex; align-items: center; gap: var(--space-xs); font-size: var(--fs-sm); color: var(--text-soft); }
.pagination-sizes .input { width: auto; height: 32px; font-size: var(--fs-sm); padding-block: 0; }
.pagination-nav { display: flex; align-items: center; gap: var(--space-xs); }
.pagination-pages { display: flex; gap: var(--space-2xs); }
.pagination a, .pagination span.current {
  display: grid; place-items: center;
  min-width: 32px; height: 32px;
  padding-inline: var(--space-2xs);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.pagination a:hover { border-color: var(--accent); color: var(--text-main); }
.pagination span.current { background: var(--accent-soft); border-color: var(--border-strong); color: var(--accent); font-weight: var(--fw-semibold); }
.page-info { font-size: var(--fs-sm); color: var(--text-soft); }

/* ── Forms ────────────────────────────────────────────────── */
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--space-md); }
.form-actions { display: flex; gap: var(--space-xs); justify-content: flex-end; margin-top: var(--space-lg); padding-top: var(--space-md); border-top: 1px solid var(--border-soft); }
.form-fieldset { border: 0; margin: 0; padding: 0; }
.form-fieldset legend { padding: 0; font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text-muted); margin-bottom: var(--space-xs); }

/* Toggle switch. The input stays a real checkbox for keyboard and form
   semantics; the track is drawn from its sibling. */
.toggle-label { display: flex; align-items: center; gap: var(--space-xs); cursor: pointer; padding-block: var(--space-2xs); }
.toggle-input { position: absolute; opacity: 0; width: 0; height: 0; }
.toggle-track {
  position: relative;
  width: 36px; height: 20px; flex-shrink: 0;
  background: var(--bg-input);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-full);
  transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
.toggle-track::after {
  content: '';
  position: absolute;
  top: 2px; left: 2px;
  width: 14px; height: 14px;
  border-radius: 50%;
  background: var(--text-soft);
  transition: transform var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
.toggle-input:checked + .toggle-track { background: var(--accent-soft); border-color: var(--accent); }
.toggle-input:checked + .toggle-track::after { transform: translateX(16px); background: var(--accent); }
.toggle-input:focus-visible + .toggle-track { outline: 2px solid var(--focus); outline-offset: 2px; }
.toggle-input:disabled + .toggle-track { opacity: 0.45; }
.toggle-text { font-size: var(--fs-md); }

/* Password/secret reveal. The button targets its input by id, never by
   walking the DOM, so wrapping the field in another div cannot break it. */
.field-with-reveal { display: flex; gap: var(--space-2xs); }
.field-with-reveal .input { flex: 1; }
.field-reveal-btn {
  width: 38px; flex-shrink: 0;
  display: grid; place-items: center;
  background: var(--bg-input);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  color: var(--text-soft);
}
.field-reveal-btn:hover { color: var(--text-main); border-color: var(--border-strong); }

/* ── Flash ────────────────────────────────────────────────── */
.flash {
  position: fixed;
  top: var(--space-md); right: var(--space-md);
  z-index: var(--z-toast);
  display: flex; align-items: center; gap: var(--space-sm);
  max-width: 420px;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lift);
  font-size: var(--fs-md);
  animation: flash-in var(--dur-med) var(--ease-spring);
}
@keyframes flash-in { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
.flash-success { background: var(--bg-card); border-color: var(--ok-line);   color: var(--ok-ink); }
.flash-error   { background: var(--bg-card); border-color: var(--bad-line);  color: var(--bad-ink); }
.flash-warning { background: var(--bg-card); border-color: var(--warn-line); color: var(--warn-ink); }
.flash-close { background: none; border: 0; color: inherit; opacity: 0.7; font-size: var(--fs-lg); line-height: 1; }
.flash-close:hover { opacity: 1; }

/* ── Modal ────────────────────────────────────────────────
   One vocabulary across both portals: .modal is the overlay,
   .modal-panel is the box, .modal-close closes it. The two
   portals previously used these names to mean opposite
   things, and hid the overlay with a server-rendered inline
   style that CSS knew nothing about. */
.modal {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-md);
}
.modal[hidden] { display: none; }
.modal-scrim { position: absolute; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(2px); }
.modal-panel {
  position: relative;
  width: 100%;
  max-width: 520px;
  max-height: calc(100vh - var(--space-xl));
  display: flex;
  flex-direction: column;
  background: var(--bg-card);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-modal);
  animation: modal-in var(--dur-med) var(--ease-out);
}
@keyframes modal-in { from { opacity: 0; transform: translateY(8px) scale(0.99); } to { opacity: 1; transform: none; } }
.modal-panel--sm { max-width: 400px; }
.modal-panel--lg { max-width: 720px; }
.modal-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); padding: var(--space-md) var(--space-lg); border-bottom: 1px solid var(--border-soft); }
.modal-title { font-size: var(--fs-lg); font-weight: var(--fw-semibold); }
.modal-close { display: grid; place-items: center; width: 30px; height: 30px; background: none; border: 0; border-radius: var(--radius-sm); color: var(--text-soft); font-size: var(--fs-lg); line-height: 1; }
.modal-close:hover { background: var(--bg-card-strong); color: var(--text-main); }
.modal-body { padding: var(--space-lg); overflow-y: auto; }
.modal-foot { display: flex; gap: var(--space-xs); justify-content: flex-end; padding: var(--space-md) var(--space-lg); border-top: 1px solid var(--border-soft); }

/* ── Locked feature ───────────────────────────────────────
   Shows what a higher plan unlocks instead of a bare "Not
   included". The feature stays visible but dimmed, so the
   difference between plans is seen rather than described. */
.locked {
  position: relative;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-lg);
  padding: var(--space-lg);
  text-align: center;
}
.locked-title { font-size: var(--fs-md); font-weight: var(--fw-semibold); }
.locked-desc { margin-top: var(--space-2xs); font-size: var(--fs-sm); color: var(--text-muted); }
.locked .btn { margin-top: var(--space-md); }
.upgrade-note { font-size: var(--fs-xs); color: var(--text-soft); }

/* ── Plan and add-ons ─────────────────────────────────────── */
.plan-summary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-lg) var(--space-2xl);
  align-items: start;
}
/* The plan sits above both lists rather than inside one of them, so Features
   and Limits start at the same line instead of a heading apart. */
.plan-headline { margin-bottom: var(--space-lg); }
.plan-features h3, .plan-limits h3 {
  margin-bottom: var(--space-2xs);
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  color: var(--text-muted);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
}
@media (max-width: 900px) {
  .plan-summary { grid-template-columns: minmax(0, 1fr); }
}
.plan-name { font-family: var(--font-display); font-size: var(--fs-xl); font-weight: var(--fw-bold); letter-spacing: var(--ls-tight); }
.plan-price { margin-top: var(--space-2xs); font-size: var(--fs-md); color: var(--text-muted); }
.plan-limits, .plan-features { display: flex; flex-direction: column; gap: var(--space-2xs); }
.limit-item, .feature-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm);
  padding-block: var(--space-2xs);
  font-size: var(--fs-sm);
  border-bottom: 1px solid var(--border-soft);
}
.limit-item:last-child, .feature-row:last-child { border-bottom: 0; }
.limit-item span:first-child, .feature-row span:first-child { color: var(--text-muted); }
.limit-item strong { font-family: var(--font-mono); font-weight: var(--fw-semibold); }

/* One card holds every category and every add-on, and each add-on stays a
   block of its own inside it, because a list of thin rules does not read as a
   set of things you can buy.
   The price and control columns are fixed rather than sized to their contents:
   each row is its own grid, so content-sized columns land at a different x on
   every line and the prices refuse to line up down the card. */
.addon-panel { padding: var(--space-md) var(--space-lg); }

.addon-category + .addon-category { margin-top: var(--space-lg); }
.addon-grid { display: flex; flex-direction: column; gap: var(--space-2xs); margin-top: var(--space-2xs); }

.addon-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 7rem 9.5rem;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  background: var(--bg-card-strong);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  transition: border-color var(--dur-fast) var(--ease-out);
}
.addon-row:hover { border-color: var(--border-strong); }
.addon-row.addon-active { border-color: var(--accent); }
.addon-main { min-width: 0; }

@media (max-width: 720px) {
  .addon-row { grid-template-columns: minmax(0, 1fr) 7rem; }
  .addon-control { grid-column: 1 / -1; }
}

/* Long enough to type a name into, not so long that it becomes the widest
   thing on the page. */
.addon-search { max-width: 18rem; }
.addon-card-header { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-xs); }
.addon-name { font-size: var(--fs-md); font-weight: var(--fw-semibold); }
.addon-price { font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--accent); white-space: nowrap; text-align: right; }
.addon-desc { margin-top: var(--space-2xs); font-size: var(--fs-sm); color: var(--text-muted); line-height: var(--lh-relaxed); }
/* These used to be cards in a grid, where margin-top:auto lined the controls
   up along the bottom edge. In a row the control sits beside what it controls,
   so it needs neither. */
.addon-control { display: flex; align-items: center; gap: var(--space-xs); }
.addon-qty-input { width: 68px; }

/* ── Switchers ────────────────────────────────────────────── */
.period-switcher { display: inline-flex; gap: var(--space-3xs); padding: var(--space-3xs); background: var(--bg-input); border: 1px solid var(--border-soft); border-radius: var(--radius-md); }
.period-switcher a {
  padding: var(--space-2xs) var(--space-sm);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.period-switcher a:hover { color: var(--text-main); }
.period-switcher a.active { background: var(--bg-card-strong); color: var(--accent); font-weight: var(--fw-semibold); }

/* ── Trend chart ──────────────────────────────────────────
   The dot and crosshair are HTML layered over the SVG, not SVG shapes.
   The chart uses preserveAspectRatio="none" and stretches non-uniformly,
   which turns an SVG circle into an ellipse. This is deliberate; do not
   "simplify" it back into a <circle>. */
.trend-chart-wrap { position: relative; }
.trend-chart { display: block; width: 100%; height: 120px; }
.chart-crosshair, .chart-dot, .chart-tooltip {
  position: absolute;
  top: 0; left: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.is-hovered .chart-crosshair, .is-hovered .chart-dot, .is-hovered .chart-tooltip { opacity: 1; }
.chart-crosshair { width: 1px; background: var(--border-strong); }
.chart-dot {
  width: 9px; height: 9px;
  margin: -4px 0 0 -4px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.chart-tooltip {
  top: auto; bottom: 100%;
  margin-bottom: var(--space-2xs);
  padding: var(--space-2xs) var(--space-xs);
  background: var(--bg-card-strong);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  font-size: var(--fs-xs);
  white-space: nowrap;
  box-shadow: var(--shadow-card);
}
.trend-delta { display: inline-flex; align-items: center; gap: var(--space-2xs); font-size: var(--fs-sm); }
.trend-up { color: var(--bad-ink); }
.trend-down { color: var(--ok-ink); }
.trend-flat { color: var(--text-soft); }

/* ── Detail rows ──────────────────────────────────────────
   Label and value on one line, used on the account and status page
   detail screens. */
.info-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-xs);
  flex-wrap: wrap;
  padding-block: var(--space-2xs);
  font-size: var(--fs-sm);
}
.info-row + .info-row { border-top: 1px solid var(--border-soft); }
.info-row > span:first-child { color: var(--text-muted); min-width: 7ch; }

/* ── Misc ─────────────────────────────────────────────────── */
.datetime-row { display: flex; gap: var(--space-2xs); }
.datetime-row .input { flex: 1; }

/* Slack and Teams message previews. Deliberately not themed with our own
   tokens: the point is to show what the message looks like in their app. */
.msg-preview-slack, .msg-preview-teams, .msg-preview-generic {
  padding: var(--space-sm);
  border-radius: var(--radius-md);
  font-size: var(--fs-sm);
  line-height: var(--lh-relaxed);
}
.msg-preview-slack { background: #1A1D21; border-left: 3px solid var(--ok-ink); color: #D1D2D3; }
.msg-preview-teams { background: #F5F5F5; border-left: 3px solid #6264A7; color: #252423; }
.msg-preview-generic { background: var(--bg-input); border: 1px solid var(--border-soft); font-family: var(--font-mono); color: var(--text-muted); overflow-x: auto; }

/* ── Layout helpers ───────────────────────────────────────
   These class names were already in the markup on both sides and had no
   rules behind them, which is why several screens looked unfinished. */
.form-row { display: flex; flex-wrap: wrap; gap: var(--space-md); }
.form-row > * { flex: 1 1 220px; min-width: 0; }
.form-inline, .inline-form { display: inline-flex; align-items: center; gap: var(--space-xs); flex-wrap: wrap; }
.form-inline-group { display: flex; align-items: flex-end; gap: var(--space-xs); flex-wrap: wrap; }
.two-col-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--space-md); }
.page-header-left { display: flex; flex-direction: column; min-width: 0; }
.page-header-right { display: flex; align-items: center; gap: var(--space-xs); flex-wrap: wrap; }
.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  margin-bottom: var(--space-md);
}
.card-header h2, .card-header h3 { font-size: var(--fs-lg); font-weight: var(--fw-semibold); }

.form-hint { font-size: var(--fs-xs); color: var(--text-soft); margin-top: var(--space-3xs); }
.form-label { font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--text-muted); }
.filter-input { height: 34px; font-size: var(--fs-sm); }
.input-disabled, .input:disabled { opacity: 0.55; cursor: not-allowed; }
.btn-warning { background: var(--warn-ink); color: #2A1503; font-weight: var(--fw-semibold); }
.btn-warning:hover { background: #FDBA74; color: #2A1503; }
.btn-select-date { flex-shrink: 0; }

/* Long URLs in a table cell must not stretch the row. */
.url-truncate {
  display: inline-block;
  max-width: 32ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
}

.ssl-warn-banner {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  margin-bottom: var(--space-md);
  border: 1px solid var(--warn-line);
  border-left: 3px solid var(--warn-ink);
  border-radius: var(--radius-md);
  background: var(--warn-fill);
  color: var(--warn-ink);
  font-size: var(--fs-sm);
}

/* ── Break glass ──────────────────────────────────────────
   Emergency admin sign-in. Deliberately understated: it should be
   findable when the magic-link path is down, and easy to ignore
   otherwise. */
.break-glass { margin-top: var(--space-lg); padding-top: var(--space-md); border-top: 1px solid var(--border-soft); }
.break-glass > summary {
  cursor: pointer;
  font-size: var(--fs-sm);
  color: var(--text-soft);
  list-style: none;
}
.break-glass > summary::-webkit-details-marker { display: none; }
.break-glass > summary:hover { color: var(--text-muted); }
.break-glass-form { margin-top: var(--space-md); }

/* ── Plan and add-on detail ───────────────────────────────── */
.addon-cat-title { font-size: var(--fs-sm); font-weight: var(--fw-semibold); text-transform: uppercase; letter-spacing: var(--ls-wide); color: var(--text-soft); }
.addon-price-period, .plan-price-period { font-size: var(--fs-xs); color: var(--text-soft); }
.addon-qty-label { font-size: var(--fs-xs); color: var(--text-muted); }
.plan-billing { font-size: var(--fs-sm); color: var(--text-muted); }
.limit-label, .feature-key { color: var(--text-muted); }
.limit-val, .feature-val { font-family: var(--font-mono); font-weight: var(--fw-semibold); }
.danger-zone-body { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); flex-wrap: wrap; }
.danger-zone-info { font-size: var(--fs-sm); color: var(--text-muted); max-width: 60ch; }

/* ── Message previews ─────────────────────────────────────
   Rendered in each tool's own colours rather than ours: the point is to
   show what the message will look like where it lands. */
.msg-preview-slack-header { display: flex; align-items: center; gap: var(--space-xs); margin-bottom: var(--space-2xs); }
.msg-preview-slack-avatar {
  display: grid; place-items: center;
  width: 22px; height: 22px; flex-shrink: 0;
  border-radius: var(--radius-sm);
  background: #4A154B;
  color: #FFFFFF;
  font-size: var(--fs-2xs);
  font-weight: var(--fw-bold);
}
.msg-preview-slack-bot { font-weight: var(--fw-bold); color: #E8E8E8; }
.msg-preview-slack-time { font-size: var(--fs-2xs); color: #9B9C9E; }
.msg-preview-slack-body { color: #D1D2D3; }
.msg-preview-slack-text { white-space: pre-wrap; }
.msg-preview-teams-title { font-weight: var(--fw-bold); color: #252423; margin-bottom: var(--space-2xs); }
.msg-preview-teams-text { color: #424242; white-space: pre-wrap; }

/* ── Danger zone ──────────────────────────────────────────── */
.danger-zone { border-color: var(--bad-line); }
.danger-zone .card-head { border-bottom-color: var(--bad-line); }
.danger-zone .card-title { color: var(--bad-ink); }

/* ── Responsive ───────────────────────────────────────────── */
@media (max-width: 900px) {
  .sidebar-toggle { display: flex; }
  .sidebar {
    transform: translateX(calc(-1 * var(--sidebar-w)));
    transition: transform var(--dur-slow) var(--ease-out);
  }
  .sidebar.open { transform: none; box-shadow: 4px 0 32px rgba(0,0,0,0.55); }
  .sidebar.open ~ .sidebar-scrim { display: block; }
  .app-main { margin-left: 0; }
  /* Clears the fixed toggle so it never covers the account name. */
  .topbar { padding-left: 60px; }
  .page-body { padding: var(--space-md); }
}

