/* Built by design-system/sync.sh from:
     00-fonts.css
     01-tokens.css
     themes/marketing.css
     02-base.css
     03-components.css
     04-marketing.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;
  /* Yellow. For a minor incident: worth noticing, not an outage. Kept apart
     from info (purple), which is the colour of planned maintenance. */
  --caution-fill: rgba(234,179,8,0.12);
  --caution-line: rgba(234,179,8,0.30);
  --caution-ink:  #FACC15;
  --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: Marketing + Status
   ---------------------------------------------------------
   GENERATED FILE. Do not edit inside an app.
   Source: design-system/css/themes/marketing.css
   ---------------------------------------------------------
   The public face. Marketing and the status pages deliberately
   share one identity: a visitor who lands on a status page
   should recognise it as the same product as the website.

   This is the only theme with a light mode, because status
   pages honour a per-customer light/dark/auto setting.
   ========================================================= */

: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:           #22D3EE;
  --accent-strong:    #67E8F9;
  --accent-ink:       #06222B;   /* text on an accent fill */
  --accent-soft:      rgba(34,211,238,0.10);
  --accent-glow:      rgba(34,211,238,0.28);

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

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

  /* Ambient glow behind heroes and auth cards. Two stops so it
     reads as light falling on the page rather than a flat blob. */
  --glow-1: rgba(34,211,238,0.16);
  --glow-2: rgba(99,102,241,0.12);
}

/* Explicit light choice, and the OS preference when the page has
   made no choice. The two blocks are identical on purpose; a
   status page renders data-theme only for an explicit setting, and
   auto mode depends on the attribute being ABSENT. Never emit
   data-theme="auto", because html:not([data-theme]) would stop matching
   and every auto-mode visitor would be pinned to dark. */
:root[data-theme="light"],
:root:not([data-theme]) {
  --light-bg-page:        #F5F8FC;
  --light-bg-shell:       #FFFFFF;
  --light-bg-card:        #FFFFFF;
  --light-bg-card-strong: #EEF3F9;
  --light-bg-input:       #FFFFFF;
  --light-text-main:      #0C1B2A;
  --light-text-muted:     #47617A;
  --light-text-soft:      #7B92A6;
  --light-accent:         #0891B2;
  --light-accent-strong:  #0E7490;
  --light-accent-ink:     #FFFFFF;
  --light-accent-soft:    rgba(8,145,178,0.08);
  --light-accent-glow:    rgba(8,145,178,0.18);
  --light-border-soft:    rgba(12,27,42,0.09);
  --light-border-strong:  rgba(12,27,42,0.18);
  --light-shadow-card:    0 4px 24px rgba(12,27,42,0.10), 0 1px 4px rgba(12,27,42,0.06);
  --light-glow-1:         rgba(8,145,178,0.12);
  --light-glow-2:         rgba(99,102,241,0.08);
}

:root[data-theme="light"] {
  color-scheme: light;
  --bg-page:        var(--light-bg-page);
  --bg-shell:       var(--light-bg-shell);
  --bg-card:        var(--light-bg-card);
  --bg-card-strong: var(--light-bg-card-strong);
  --bg-input:       var(--light-bg-input);
  --text-main:      var(--light-text-main);
  --text-muted:     var(--light-text-muted);
  --text-soft:      var(--light-text-soft);
  --accent:         var(--light-accent);
  --accent-strong:  var(--light-accent-strong);
  --accent-ink:     var(--light-accent-ink);
  --accent-soft:    var(--light-accent-soft);
  --accent-glow:    var(--light-accent-glow);
  --border-soft:    var(--light-border-soft);
  --border-strong:  var(--light-border-strong);
  --shadow-card:    var(--light-shadow-card);
  --glow-1:         var(--light-glow-1);
  --glow-2:         var(--light-glow-2);
  --ok-fill:   rgba(22,163,74,0.10);
  --ok-ink:    #15803D;
  --warn-fill: rgba(234,88,12,0.10);
  --warn-ink:  #C2410C;
  --bad-fill:  rgba(220,38,38,0.10);
  --bad-ink:   #B91C1C;
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    color-scheme: light;
    --bg-page:        var(--light-bg-page);
    --bg-shell:       var(--light-bg-shell);
    --bg-card:        var(--light-bg-card);
    --bg-card-strong: var(--light-bg-card-strong);
    --bg-input:       var(--light-bg-input);
    --text-main:      var(--light-text-main);
    --text-muted:     var(--light-text-muted);
    --text-soft:      var(--light-text-soft);
    --accent:         var(--light-accent);
    --accent-strong:  var(--light-accent-strong);
    --accent-ink:     var(--light-accent-ink);
    --accent-soft:    var(--light-accent-soft);
    --accent-glow:    var(--light-accent-glow);
    --border-soft:    var(--light-border-soft);
    --border-strong:  var(--light-border-strong);
    --shadow-card:    var(--light-shadow-card);
    --glow-1:         var(--light-glow-1);
    --glow-2:         var(--light-glow-2);
    --ok-fill:   rgba(22,163,74,0.10);
    --ok-ink:    #15803D;
    --warn-fill: rgba(234,88,12,0.10);
    --warn-ink:  #C2410C;
    --bad-fill:  rgba(220,38,38,0.10);
    --bad-ink:   #B91C1C;
  }
}

/* =========================================================
   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 {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-family: var(--font-display);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-tight);
  color: var(--text-main);
}
/* Sized from the type it stands next to, so it follows wherever the
   wordmark is set larger or smaller. */
.wordmark .brand-mark { width: 1.3em; height: 1.3em; }

@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);
}
/* The mark is an image now (design-system/logo), not a letter in a tile.
   Where a page can be light or dark the markup carries both versions and
   these rules show the one that fits, with the same two conditions the
   theme itself uses: an explicit choice, or the OS preference when the
   page has made none. Dark is the default, so the portals, which are only
   ever dark, need nothing more than the one image. */
.brand-mark {
  display: block;
  width: 30px;
  height: 30px;
  flex-shrink: 0;
}
.brand-mark-light { display: none; }
:root[data-theme="light"] .brand-mark-dark { display: none; }
:root[data-theme="light"] .brand-mark-light { display: block; }
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .brand-mark-dark { display: none; }
  :root:not([data-theme]) .brand-mark-light { display: block; }
}
.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-caution { background: var(--caution-fill); border-color: var(--caution-line); color: var(--caution-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);
}
/* The chosen option is marked by its background and colour, not by a
   heavier weight. Bold text is wider, so "Yearly, two months free" fitted on
   one line until it was selected and then wrapped onto two, and the whole
   control grew taller the moment it was clicked.

   Each option is as wide as its own label needs, plus an equal share of what
   is left, and stays on one line. On a narrow phone there is not room for
   that, and there it may wrap, in both states alike. */
.seg-option {
  flex: 1 1 auto;
  padding: var(--space-2xs) var(--space-xs);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
  text-align: center;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
@media (max-width: 480px) {
  .seg-option { white-space: normal; }
}
.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);
}
.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: Marketing layer
   ---------------------------------------------------------
   GENERATED FILE. Do not edit inside an app.
   Source: design-system/css/04-marketing.css
   ---------------------------------------------------------
   Loaded only by the public marketing site. This is the one
   surface where scroll choreography belongs; the portals get
   none of it, because motion in a screen full of tables is
   noise rather than polish.
   ========================================================= */

/* Sticky-header offset for in-page anchors, so a jumped-to
   heading never hides behind the topbar. */
:where(h1, h2, h3, [id]) { scroll-margin-top: calc(var(--topbar-h) + var(--space-lg)); }

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

/* ── Ambient glow ─────────────────────────────────────────
   Fixed, behind everything, cheap. Two offset radials so the
   page reads as lit from above rather than tinted. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(60rem 40rem at 15% -10%, var(--glow-1), transparent 65%),
    radial-gradient(50rem 35rem at 85% 5%,  var(--glow-2), transparent 60%);
}

/* ── Topbar ───────────────────────────────────────────────── */
.topbar {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  height: var(--topbar-h);
  display: flex;
  align-items: center;
  background: color-mix(in srgb, var(--bg-page) 78%, transparent);
  backdrop-filter: blur(14px) saturate(140%);
  border-bottom: 1px solid var(--border-soft);
}
.topbar-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  width: 100%;
}
.topbar .wordmark { font-size: var(--fs-lg); }

.nav-links { display: flex; align-items: center; gap: var(--space-lg); }
.nav-links a {
  color: var(--text-muted);
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
}
.nav-links a:hover { color: var(--text-main); }
.nav-cta { display: flex; align-items: center; gap: var(--space-xs); }

.nav-toggle {
  display: none;
  width: 40px; height: 40px;
  align-items: center; justify-content: center;
  background: transparent;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  color: var(--text-muted);
}

/* ── Hero ─────────────────────────────────────────────────── */
.hero {
  padding-top: var(--space-3xl);
  padding-bottom: var(--space-2xl);
  text-align: center;
}
.hero h1 {
  font-size: var(--fs-4xl);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-tight);
  margin-top: var(--space-md);
  max-width: 18ch;
  margin-inline: auto;
}
.hero h1 .accent {
  background: linear-gradient(100deg, var(--accent) 0%, var(--accent-strong) 60%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.hero-lede {
  margin: var(--space-lg) auto 0;
  max-width: 56ch;
  font-size: var(--fs-base);
  color: var(--text-muted);
  line-height: var(--lh-relaxed);
}
.hero-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-sm);
  margin-top: var(--space-xl);
}
.hero-note {
  margin-top: var(--space-md);
  font-size: var(--fs-sm);
  color: var(--text-soft);
}

/* ── Status page mock ─────────────────────────────────────
   Mirrors the real status-app markup and colours so the
   homepage shows the actual product, not an illustration of it.
   Status colours come from the shared tokens, which is exactly
   why they match the live page. */
.hero-visual {
  position: relative;
  max-width: 620px;
  margin: var(--space-2xl) auto 0;
}
.hero-visual::before {
  content: '';
  position: absolute;
  inset: -12% -6% 10%;
  background: radial-gradient(50% 50% at 50% 40%, var(--accent-glow), transparent 70%);
  filter: blur(48px);
  z-index: -1;
}
.mock {
  background: var(--bg-card);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-lift);
  overflow: hidden;
  text-align: left;
}
.mock-top {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-sm) var(--space-md);
  border-bottom: 1px solid var(--border-soft);
  background: var(--bg-card-strong);
}
.mock-body { padding: var(--space-md); display: flex; flex-direction: column; gap: var(--space-sm); }
.mock-banner {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-md);
  background: var(--ok-fill);
  border: 1px solid var(--ok-line);
  color: var(--ok-ink);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-md);
}
/* The banner says what the services under it say. One of them degraded is
   not "all systems operational", on the real page or in a picture of it. */
.mock-banner.is-warn { background: var(--warn-fill); border-color: var(--warn-line); color: var(--warn-ink); }
.mock-rows { display: flex; flex-direction: column; }
.mock-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: var(--space-sm) 0;
  font-size: var(--fs-sm);
  border-bottom: 1px solid var(--border-soft);
}
.mock-row:last-child { border-bottom: 0; }
.mock-incident {
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--warn-line);
  border-left: 3px solid var(--warn-ink);
  border-radius: var(--radius-md);
  background: var(--bg-card-strong);
}
.mock-incident-top { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-xs); }
.mock-incident-title { font-size: var(--fs-sm); font-weight: var(--fw-semibold); }
.mock-incident-time { margin-top: var(--space-2xs); font-size: var(--fs-xs); color: var(--text-soft); }

/* ── App screens (product showcase) ───────────────────────
   The same coded-not-screenshotted approach as the status mock
   above, extended to the two portal screens. Everything here
   mirrors a real screen in customer-portal: the sidebar's own
   order, the four stat cards on /dashboard, and the column set
   of its "Latest check results" table.

   A screenshot would have been less work and worse: it goes
   stale the moment the portal changes, it carries whatever real
   account data happened to be on screen, and it cannot respond
   to a phone's width. This renders from the same tokens the
   portal renders from, so a change to a surface colour or a
   status tone reaches the marketing site in the same build. */
.mock-app { display: grid; grid-template-columns: 9.5rem minmax(0, 1fr); }

/* The portal's accent, not marketing's. The two themes differ in
   exactly this one value (see design-system/css/themes/portal.css);
   everything else about the surfaces is already shared, which is
   why re-pointing the accent is enough to make the panel look like
   the real app rather than like the website wearing app furniture.
   If the portal's accent changes, change it here too. */
.mock-app[data-app="portal"] {
  --accent:        #38BDF8;
  --accent-strong: #7DD3FC;
  --accent-soft:   rgba(56,189,248,0.10);
}

.mock-rail {
  border-right: 1px solid var(--border-soft);
  background: var(--bg-shell);
  padding: var(--space-sm);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}
.mock-rail .brand-mark { width: 20px; height: 20px; }
.mock-brand {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-2xs) var(--space-xs) var(--space-sm);
}
.mock-brand-name { font-size: var(--fs-sm); font-weight: var(--fw-semibold); }
/* The switch between an agency's own pages and its clients', where it sits
   in the portal: at the top of the sidebar, above the menu it changes. */
.mock-ctx {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: var(--space-2xs) var(--space-xs);
  margin-bottom: var(--space-xs);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
}
.mock-ctx-label { font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: var(--ls-wide); color: var(--text-soft); }
.mock-ctx-current { font-size: var(--fs-xs); font-weight: var(--fw-semibold); }
.mock-nav { display: flex; flex-direction: column; gap: 1px; }
/* The tabs every screen about one status page carries, as a strip. */
.mock-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  padding-bottom: var(--space-2xs);
  border-bottom: 1px solid var(--border-soft);
  font-size: var(--fs-xs);
  color: var(--text-muted);
}
.mock-tabs .is-active { color: var(--text-main); font-weight: var(--fw-semibold); }
.mock-nav-item {
  display: block;
  padding: var(--space-2xs) var(--space-xs);
  border-radius: var(--radius-sm);
  font-size: var(--fs-xs);
  color: var(--text-muted);
}
.mock-nav-item.is-active {
  background: var(--accent-soft);
  color: var(--accent-strong);
  font-weight: var(--fw-semibold);
}
.mock-nav-label {
  padding: var(--space-sm) var(--space-xs) var(--space-3xs);
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-soft);
}

.mock-screen { padding: var(--space-md); display: flex; flex-direction: column; gap: var(--space-sm); min-width: 0; }
.mock-h1 { font-size: var(--fs-md); font-weight: var(--fw-semibold); }
.mock-sub { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 1px; }
.mock-section-title {
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  color: var(--text-muted);
  margin-top: var(--space-2xs);
}

.mock-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-xs); }
.mock-stat {
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  background: var(--bg-card-strong);
  padding: var(--space-xs) var(--space-sm);
  min-width: 0;
}
.mock-stat-label { font-size: var(--fs-2xs); color: var(--text-muted); }
.mock-stat-value { font-size: var(--fs-lg); font-weight: var(--fw-semibold); line-height: var(--lh-tight); margin-top: 1px; }
.mock-stat-value.is-text { font-size: var(--fs-sm); }
.mock-stat-hint { font-size: var(--fs-2xs); color: var(--text-soft); margin-top: 1px; }

.mock-tbl { width: 100%; border-collapse: collapse; font-size: var(--fs-xs); }
.mock-tbl th {
  text-align: left;
  font-size: var(--fs-2xs);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-soft);
  padding: var(--space-2xs) var(--space-xs);
  border-bottom: 1px solid var(--border-soft);
  white-space: nowrap;
}
/* Only the figures hold their line. A service name or an incident title is
   prose: forcing it onto one line is what pushed the table past the panel's
   edge, where the mock's own overflow:hidden cut the last column off. */
.mock-tbl td {
  padding: var(--space-2xs) var(--space-xs);
  border-bottom: 1px solid var(--border-soft);
  color: var(--text-main);
}
.mock-tbl td .pill { white-space: nowrap; }
.mock-tbl tr:last-child td { border-bottom: 0; }
.mock-tbl .num { font-family: var(--font-mono); color: var(--text-muted); white-space: nowrap; }
.mock-tbl .soft { color: var(--text-soft); }

/* The lifecycle, as the status page prints it: one row per event,
   oldest at the bottom, each with the time it actually happened. */
.mock-timeline { display: flex; flex-direction: column; }
.mock-tl {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-xs);
  padding: var(--space-xs) 0;
  border-bottom: 1px solid var(--border-soft);
}
.mock-tl:last-child { border-bottom: 0; }
.mock-tl-rail { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.mock-tl-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-soft); margin-top: 5px; }
.mock-tl-dot.tone-bad { background: var(--bad-ink); }
.mock-tl-dot.tone-warn { background: var(--warn-ink); }
.mock-tl-dot.tone-info { background: var(--accent); }
.mock-tl-dot.tone-ok { background: var(--ok-ink); }
.mock-tl-title { font-size: var(--fs-xs); font-weight: var(--fw-semibold); }
.mock-tl-note { font-size: var(--fs-2xs); color: var(--text-muted); margin-top: 1px; }
.mock-tl-time { font-size: var(--fs-2xs); color: var(--text-soft); font-family: var(--font-mono); margin-top: 1px; }

/* ── Showcase ─────────────────────────────────────────────
   Caption and screen side by side, sides alternating down the
   page. The screen is the wider column: it is the thing being
   shown, and the caption is there to say what to look at. */
/* Said once, plainly, above the panels. They are drawings of the screens
   rather than a window onto a running account, and a visitor who assumes
   otherwise and then notices the numbers never move has been misled by
   omission. */
.showcase-note {
  margin: 0 auto var(--space-xl);
  max-width: 52ch;
  text-align: center;
  font-size: var(--fs-sm);
  color: var(--text-soft);
}
.showcase { display: flex; flex-direction: column; gap: var(--space-2xl); }
.showcase-item {
  display: grid;
  grid-template-columns: minmax(0, 0.78fr) minmax(0, 1.52fr);
  gap: var(--space-xl);
  align-items: center;
}
/* Flipping the sides means flipping the track widths too, not only the
   order. `order` alone moved the caption to the second column and handed
   the screen the narrow one, so every other screen came out half-size. */
.showcase-item:nth-child(even) { grid-template-columns: minmax(0, 1.52fr) minmax(0, 0.78fr); }
.showcase-item:nth-child(even) > .showcase-caption { order: 2; }
.showcase-caption h3 { font-size: var(--fs-xl); letter-spacing: -0.01em; }
.showcase-caption p { margin-top: var(--space-xs); color: var(--text-muted); line-height: var(--lh-relaxed); }
.showcase-caption .eyebrow { margin-bottom: var(--space-2xs); }
.showcase-screen { position: relative; min-width: 0; }
/* The status mock carries the hero's own width cap and top margin, because
   that is where it normally lives. Inside the showcase it is one panel of
   three and has to match the other two. */
.showcase-screen .hero-visual { max-width: none; margin: 0; }
.showcase-screen .hero-visual::before { display: none; }
.showcase-screen::before {
  content: '';
  position: absolute;
  inset: -8% -4% 6%;
  background: radial-gradient(50% 50% at 50% 40%, var(--accent-glow), transparent 70%);
  filter: blur(48px);
  z-index: -1;
}

@media (max-width: 900px) {
  .showcase-item,
  .showcase-item:nth-child(even) { grid-template-columns: minmax(0, 1fr); gap: var(--space-md); }
  .showcase-item:nth-child(even) > .showcase-caption { order: 0; }
  .mock-app { grid-template-columns: minmax(0, 1fr); }
  /* The rail is navigation, and navigation is the first thing worth
     dropping when the screen it belongs to has to fit a phone. */
  .mock-rail { display: none; }
  .mock-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .mock-scroll { overflow-x: auto; }
}

/* ── Logo / trust strip ───────────────────────────────────── */
/* A grid rather than a wrapping flex row: once each item carries a
   description the widths differ enough that flex-wrap leaves an orphan on
   its own line. Fixed columns keep the four aligned as a set. */
.trust {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-lg);
  margin-top: var(--space-2xl);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--border-soft);
}
.trust-label {
  grid-column: 1 / -1;
  text-align: center;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
  color: var(--text-soft);
}
.trust-item {
  display: inline-flex;
  align-items: flex-start;
  gap: var(--space-xs);
  text-align: left;
}
.trust-item svg { width: 18px; height: 18px; flex-shrink: 0; margin-top: 1px; color: var(--accent); }
.trust-item-name { display: block; font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text-main); }
/* Availability sits with the check itself. Two of these four are not on
   every plan, and a strip that implies otherwise is a promise the
   pricing page then contradicts. */
.trust-item-note { display: block; font-size: var(--fs-xs); color: var(--text-soft); margin-top: 1px; }

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

/* ── Steps ────────────────────────────────────────────────── */
.steps {
  --step-size: 34px;   /* the rail is positioned from this, so they cannot drift apart */
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-lg);
}
.steps-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.step { position: relative; }
/* Connector rail from each circle to the next. The circles are left
   aligned within their column, not centred, so the rail is measured
   from the circle rather than from the column midpoint.
   The same --space-sm breathing room sits on both ends: touching the
   next circle while standing clear of the previous one reads as a
   misalignment even when the geometry is exact. */
.step:not(:last-child)::before {
  content: '';
  position: absolute;
  top: calc(var(--step-size) / 2);                    /* centre of the circle */
  left: calc(var(--step-size) + var(--space-sm));
  right: calc(var(--space-sm) - var(--space-lg));     /* crosses the grid gap */
  height: 1px;
  background: var(--border-strong);
}
/* At the end of a row the next step is on the line below, so the rail has
   nowhere horizontal to go. It still runs, but stops at the column's own
   edge instead of crossing a gap that is not there: the sequence carries
   on, and the line says so without pointing into the margin. The rule
   above already keeps it off the very last step, where it would be a
   promise of a seventh. */
.steps-3 .step:nth-child(3n)::before { right: 0; }
.step-num {
  display: grid;
  place-items: center;
  width: var(--step-size); height: var(--step-size);
  margin-bottom: var(--space-sm);
  border-radius: 50%;
  background: var(--accent-soft);
  border: 1px solid var(--border-strong);
  color: var(--accent);
  font-family: var(--font-display);
  font-weight: var(--fw-bold);
  font-size: var(--fs-sm);
}
.step h3 { font-size: var(--fs-base); }
.step p { margin-top: var(--space-2xs); font-size: var(--fs-sm); color: var(--text-muted); }

@media (max-width: 820px) {
  .steps { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The rail only makes sense on a single unbroken row. */
  .step::before { display: none; }
}
@media (max-width: 520px) {
  .steps { grid-template-columns: minmax(0, 1fr); }
}

/* ── Feature cards ────────────────────────────────────────── */
.feature { padding: var(--space-lg); }
.feature-icon {
  display: grid;
  place-items: center;
  width: 38px; height: 38px;
  margin-bottom: var(--space-sm);
  border-radius: var(--radius-md);
  background: var(--accent-soft);
  border: 1px solid var(--border-strong);
  color: var(--accent);
}
.feature-icon svg { width: 18px; height: 18px; }
.feature { display: flex; flex-direction: column; }
.feature h3 { font-size: var(--fs-base); }
.feature p { margin-top: var(--space-2xs); font-size: var(--fs-sm); color: var(--text-muted); line-height: var(--lh-relaxed); }
/* Which plans include this. flex-end pins it to the bottom so the notes
   line up across a row of cards with different copy lengths. */
.feature-note {
  align-self: flex-start;
  margin-top: auto;
  padding-top: var(--space-md);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  color: var(--text-soft);
}

/* ── Pricing ──────────────────────────────────────────────── */
.price-card { display: flex; flex-direction: column; padding: var(--space-lg); }
.price-name { font-size: var(--fs-base); font-weight: var(--fw-semibold); }
.price-tagline {
  margin-top: var(--space-3xs);
  font-size: var(--fs-sm);
  color: var(--text-muted);
  /* Two lines' worth of room whether the sentence needs them or not. One
     tagline wraps before the others do at narrower widths, and without this
     that card's whole feature list sits a line lower than its neighbours,
     which is exactly the row-by-row comparison the list exists to allow. */
  min-height: calc(2 * var(--lh-normal) * 1em);
}
.price-amount {
  display: flex;
  align-items: baseline;
  gap: var(--space-2xs);
  margin-top: var(--space-md);
}
.price-num {
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-tight);
}
.price-per { font-size: var(--fs-sm); color: var(--text-soft); }

/* Sits under the amount rather than beside it, because it qualifies the price
   and should not read as part of it. Dutch and EU rules want the VAT basis
   stated at the price itself, not in a footnote under the grid. */
/* The value sits at the end of its own line rather than under the name. On its
   own line it doubled the height of the card for no extra meaning. */
.price-feature-val {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  color: var(--text-main);
  text-align: right;
}

.price-vat {
  margin-top: var(--space-3xs);
  font-size: var(--fs-xs);
  color: var(--text-soft);
}
/* flex: 1 makes the list absorb the height difference between plans, so
   the divider and the button land on the same line in every card no
   matter how many features each one lists. */
.price-features {
  flex: 1;
  list-style: none;
  margin: var(--space-lg) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}
.price-features li { display: flex; align-items: baseline; gap: var(--space-xs); font-size: var(--fs-sm); }
.price-feature-name { flex: 1; min-width: 0; }
.price-features svg { width: 15px; height: 15px; flex-shrink: 0; margin-top: 2px; }
.price-features .yes { color: var(--ok-ink); }
.price-features .no { color: var(--text-soft); }
.price-features li.muted { color: var(--text-soft); }
.price-actions { margin-top: var(--space-lg); padding-top: var(--space-lg); border-top: 1px solid var(--border-soft); }

.price-badge {
  position: absolute;
  top: calc(-1 * var(--space-sm));
  left: 50%;
  transform: translateX(-50%);
  padding: 3px var(--space-sm);
  border-radius: var(--radius-full);
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
  white-space: nowrap;
}
/* The wrapper only exists to hang the badge off, so it has to pass the
   grid's stretched height straight through to the card. Without this
   the featured card sizes to its own content and the row loses its
   shared baseline. */
.price-wrap { position: relative; display: flex; }
.price-wrap > .card { flex: 1; }
.price-card { height: 100%; }

/* ── Price teaser ─────────────────────────────────────────
   Three names and three numbers on the homepage, because a
   visitor who has to click through to find out whether this is
   a 10-euro tool or a 400-euro one usually just leaves. The
   real comparison stays on /pricing; this is the answer to
   "roughly what am I looking at". */
.price-teaser {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-md);
  margin-top: var(--space-lg);
}
.price-teaser-item {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  padding: var(--space-md);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  background: var(--bg-card);
  text-align: center;
}
.price-teaser-name { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text-muted); }
.price-teaser-price { font-size: var(--fs-2xl); font-weight: var(--fw-bold); letter-spacing: -0.02em; }
.price-teaser-per { font-size: var(--fs-sm); font-weight: var(--fw-regular); color: var(--text-soft); }
.price-teaser-note { font-size: var(--fs-xs); color: var(--text-soft); }

/* A row of links under a section, centred, for "and now go here".
   Wraps rather than squeezing, so two buttons stay legible on a phone. */
.section-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-sm);
  margin-top: var(--space-xl);
}

@media (max-width: 700px) {
  .price-teaser { grid-template-columns: minmax(0, 1fr); }
}

/* ── FAQ ──────────────────────────────────────────────────── */
.faq { display: flex; flex-direction: column; gap: var(--space-xs); }
.faq-item {
  background: var(--bg-card);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
}
.faq-item summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  cursor: pointer;
  list-style: none;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: '+';
  flex-shrink: 0;
  font-size: var(--fs-lg);
  line-height: 1;
  color: var(--accent);
  transition: transform var(--dur-med) var(--ease-out);
}
.faq-item[open] summary::after { transform: rotate(45deg); }
.faq-item[open] { border-color: var(--border-strong); }
.faq-answer {
  padding: 0 var(--space-md) var(--space-md);
  font-size: var(--fs-sm);
  color: var(--text-muted);
  line-height: var(--lh-relaxed);
}

/* Native <details> snaps open with no way to transition it, so the
   height is animated in JS. Scoped to html.js: without the script the
   element still opens and closes perfectly well, just instantly.
   The wrapper carries the animation so the answer keeps its padding
   collapsed along with the rest of the box. */
@media (prefers-reduced-motion: no-preference) {
  html.js .faq-answer-wrap {
    overflow: hidden;
    transition: height var(--dur-med) var(--ease-out);
  }
}

/* ── CTA band ─────────────────────────────────────────────── */
.cta-band {
  padding: var(--space-2xl) var(--space-lg);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-xl);
  background:
    radial-gradient(70% 120% at 50% 0%, var(--accent-soft), transparent 70%),
    var(--bg-card);
  text-align: center;
}
.cta-band h2 { font-size: var(--fs-3xl); letter-spacing: var(--ls-tight); }
.cta-band p { margin: var(--space-sm) auto 0; max-width: 52ch; color: var(--text-muted); }
.cta-band .hero-actions { margin-top: var(--space-lg); }

/* ── Prose ────────────────────────────────────────────────── */
.prose { max-width: var(--wrap-prose); }
.prose h1 { font-size: var(--fs-3xl); }
.prose h2 { font-size: var(--fs-xl); margin-top: var(--space-xl); }
.prose p, .prose li { margin-top: var(--space-sm); color: var(--text-muted); line-height: var(--lh-relaxed); }
.prose ul { padding-left: var(--space-lg); }
.prose strong { color: var(--text-main); font-weight: var(--fw-semibold); }
.prose .updated { font-size: var(--fs-sm); color: var(--text-soft); }
.legal-notice {
  margin-top: var(--space-xl);
  padding: var(--space-md);
  border: 1px solid var(--warn-line);
  border-radius: var(--radius-md);
  background: var(--warn-fill);
  color: var(--warn-ink);
  font-size: var(--fs-sm);
}

/* ── Legal documents ──────────────────────────────────────
   Terms are read in two ways: once, top to bottom, and afterwards by
   someone looking for one clause they were told the number of. So every
   clause carries its number in a fixed gutter, where it can be found
   without reading, and stays out of the sentence, where it would only be
   in the way of the first kind of reader. */
.legal { max-width: 760px; }
.legal .clause {
  display: grid;
  grid-template-columns: 3.25rem minmax(0, 1fr);
  margin-top: var(--space-sm);
}
.legal .clause > .clause-body:only-child { grid-column: 1 / -1; }
.legal .clause-no {
  padding-top: 0.2em;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--text-soft);
}
.legal .clause-body > p:first-child { margin-top: 0; }
.legal .clause-body ul { margin-top: var(--space-2xs); }
.legal .clause-body li { margin-top: var(--space-2xs); }

/* The plain-language summary. Boxed and visibly apart from the numbered
   text, because the one thing it must never be mistaken for is a clause. */
.legal-summary {
  margin-top: var(--space-xl);
  padding: var(--space-md) var(--space-lg);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--bg-card);
}
.legal-summary h2 { margin-top: 0; font-size: var(--fs-lg); }
.legal-summary-note { font-size: var(--fs-sm); color: var(--text-soft); }

.legal-toc { margin-top: var(--space-xl); }
.legal-toc h2 { margin-top: 0; font-size: var(--fs-lg); }
.legal-toc ol {
  list-style: none;
  padding: 0;
  margin-top: var(--space-sm);
  columns: 2;
  column-gap: var(--space-xl);
}
.legal-toc li { margin-top: 0; break-inside: avoid; }
.legal-toc a {
  display: flex;
  gap: var(--space-xs);
  padding: var(--space-3xs) 0;
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.legal-toc a:hover { color: var(--accent-strong); }
.legal-toc-no {
  flex: 0 0 1.75rem;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--text-soft);
  padding-top: 0.15em;
}
.footer-reg { margin-left: var(--space-sm); white-space: nowrap; }

/* A table inside a clause, for the one thing in a legal text that really is
   tabular: who processes what, and where. */
.legal-table { margin-top: var(--space-sm); font-size: var(--fs-sm); border: 1px solid var(--border-soft); border-radius: var(--radius-md); }
.legal-table th, .legal-table td { padding: var(--space-xs) var(--space-sm); text-align: left; vertical-align: top; border-bottom: 1px solid var(--border-soft); }
.legal-table th { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-soft); }
.legal-table td { color: var(--text-muted); line-height: var(--lh-normal); }
.legal-table tr:last-child td { border-bottom: 0; }
.legal .clause-body { min-width: 0; overflow-x: auto; }

@media (max-width: 640px) {
  .legal-toc ol { columns: 1; }
  .legal .clause { grid-template-columns: 2.75rem minmax(0, 1fr); }
}

/* ── Waitlist ─────────────────────────────────────────────── */
.waitlist { display: flex; gap: var(--space-xs); max-width: 440px; margin: var(--space-lg) auto 0; }
.waitlist .input { flex: 1; }
@media (max-width: 480px) { .waitlist { flex-direction: column; } }

.flash {
  max-width: 440px;
  margin: var(--space-lg) auto 0;
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-md);
  font-size: var(--fs-md);
}
.flash-success { background: var(--ok-fill);  border: 1px solid var(--ok-line);  color: var(--ok-ink); }
.flash-error   { background: var(--bad-fill); border: 1px solid var(--bad-line); color: var(--bad-ink); }

/* ── Footer ───────────────────────────────────────────────── */
.footer {
  margin-top: var(--space-3xl);
  padding-block: var(--space-2xl) var(--space-lg);
  border-top: 1px solid var(--border-soft);
  background: var(--bg-shell);
}
.footer-grid {
  display: grid;
  grid-template-columns: 1.4fr repeat(3, minmax(0, 1fr));
  gap: var(--space-xl);
}
.footer-about { font-size: var(--fs-sm); color: var(--text-muted); margin-top: var(--space-sm); max-width: 34ch; }
.footer-col h4 {
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
  color: var(--text-soft);
  margin-bottom: var(--space-sm);
}
.footer-col a { display: block; padding-block: var(--space-3xs); font-size: var(--fs-sm); color: var(--text-muted); }
.footer-col a:hover { color: var(--text-main); }
.footer-bottom {
  margin-top: var(--space-xl);
  padding-top: var(--space-md);
  border-top: 1px solid var(--border-soft);
  font-size: var(--fs-sm);
  color: var(--text-soft);
}

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

/* ── Reveal on scroll ─────────────────────────────────────
   The pre-reveal state is applied ONLY under html.js, which the
   bundle sets on boot. If the script fails to load the content
   is simply visible, never a blank page. */
@media (prefers-reduced-motion: no-preference) {
  html.js .reveal {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out);
  }
  html.js .reveal.in { opacity: 1; transform: none; }
}

/* ── Mobile nav ───────────────────────────────────────────── */
@media (max-width: 860px) {
  .nav-toggle { display: inline-flex; }
  .nav-links {
    position: absolute;
    top: var(--topbar-h);
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: var(--space-xs);
    background: var(--bg-shell);
    border-bottom: 1px solid var(--border-soft);
    box-shadow: var(--shadow-card);
  }
  .nav-links:not(.open) { display: none; }
  .nav-links a { padding: var(--space-sm); border-radius: var(--radius-md); }
  .nav-links a:hover { background: var(--bg-card); }
  .nav-cta .btn-ghost { display: none; }
}

