/* UptimeUniversity design tokens and layout primitives.

   ONE FILE, LOADED FIRST BY EVERY PAGE — the member app (index.html), the operator
   console (admin.html), the public certificate check (/verify) and the marketing site
   (site/_layout.html) all link it ahead of their own styles. Changing a value here
   changes it everywhere, which is the point: until this file existed the product had
   115 inline styles and spacing values of 5, 6, 7, 8, 9, 10, 12 and 18px, each chosen
   once, in place, for one element.

   LAYER ORDER, and why it matters:
     ui.css        tokens (scales, the default palette, component tokens) and the
                   components every surface shares
     admin.css +   the page's own components, written against these tokens. A page's
       admin_extra   :root sets only what differs on that surface (the member app's
     index.html      roomier controls, the console's rail width, the site's palette)
       <style>
     site.css
     /theme.css    branding overrides, emitted by the server from the operator's Theme
                   and Branding screens (app pages and /verify, not the site). It only
                   redefines tokens (colours, font stacks, scale), never rules, so it
                   can always win.

   HOW TO CHANGE THE LOOK — edit a token, never a rule:
     button colour     --btn-bg / --btn-ink below (every .btn, the member send button
                       and the /verify button). Per install: the Theme screen's accent
                       (/theme.css --accent). Marketing site: site.css :root.
     button shape      --btn-radius, --btn-weight; size per surface with --control-pad-y,
                       --control-pad-x, --control-text (console: here; member app:
                       app.css :root; site: site.css :root), each a --space-* /
                       --text-* step.
     type size         --text-xs / -sm / -md / -lg / -xl / -display: the six sizes the
                       whole product uses (11, 12.5, 15, 18, 24px and a 32-48px hero
                       clamp). Every font-size reads one; the app bodies scale --text-md
                       by Branding's --font-scale. Emails write the same px values.
     spacing           --space-0..10 (2, 4, 8, 12, 16, 24, 32, 48, 64, 80, 96px),
                       --gap-container, --gap-content, --gap-inline, --pad-container
                       (the console's .pad), --pad-card (the member app's cards,
                       app.css :root). EVERY padding, margin and gap is a token;
                       tests/test_ui_consistency.py fails on a px literal (a 1px optical
                       nudge on its short list excepted).
     corners           --radius-xs..2xl, --radius-pill, --radius-bar; panels read
                       --panel-radius. Decided 2026-10-08: corners stay on this fixed
                       scale; Branding's radius slider (--r-1..4) only shapes the menu
                       items it already shapes.
     borders           --hairline (1px solid var(--edge)); the site sets its own.
     text fields       --field-bg, --field-border, --field-ink, --field-focus.
     panels            --panel-bg, --panel-border, --panel-radius, --panel-blur,
                       --panel-shadow.
     palette           the default palette block below; /theme.css overrides it per
                       install. A page :root holds only the tokens it alone uses.
     stacking          --z-*; the console raises --z-scrim and --z-drawer in admin.css.

   THE TWO RULES THIS FILE EXISTS TO ENFORCE

   1. Fonts come from tokens, never from names. `--font-body`, `--font-heading`
      and `--font-mono` are the only font families the product may use. The
      "100%" on the Overview screen rendered in Times because an inline style
      named `Inter` directly: once the Google Fonts request was removed, Inter was
      not installed, a one-name stack has nothing to fall back to, and the browser
      used its default serif. A token carries its own fallback chain, and the
      Branding screen can reach it. tests/test_ui_consistency.py fails on a
      hardcoded font family anywhere in web/.

   2. Containers never touch. `.panel` had no margin at all, so two panels in a
      row sat flush against each other unless the screen that drew them remembered
      to add spacing by hand — which is what the Overview screen's Corpus and
      Embedding Coverage panels did not do. The flow rule below separates every
      container in a page's work area by --gap-container, so a new screen inherits
      the spacing instead of having to remember it. */

:root{
  /* ---------------------------------------------------------------- spacing --
     A 4px base with a 2px micro step (as IBM Carbon and Atlassian have). Every
     margin, padding and gap in the product is one of these (owner ruling,
     2026-10-08): a value between two steps was snapped to the nearer, and a tie
     (6, 10, 14, 20, 28, 40, 56, 72px) to the larger. Only 1px optical nudges stay
     literal, on an explicit list in tests/test_ui_consistency.py. If a layout needs
     a value that is not here, the scale is wrong and should change here, not in
     one place. */
  --space-0: 2px;   /* the micro step: a badge's inset, a hairline's gap */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;  /* section and page rhythm: the site's bands, the empty states */
  --space-9: 80px;
  --space-10: 96px;

  /* The minimum distance between two containers — panels, cards, callouts,
     toolbars. Raise this one value and every screen breathes more. */
  --gap-container: var(--space-5);
  /* Between siblings inside a container: a heading and its body, rows in a list. */
  --gap-content: var(--space-3);
  /* Between items on one line: buttons, chips, a label and its value. */
  --gap-inline: var(--space-2);
  /* Inside a container, edge to content. */
  --pad-container: var(--space-4);

  /* ------------------------------------------------------------------- type --
     The app pages set their own --font-body (Inter first) and /theme.css overrides
     it. These defaults exist so a page that forgets one still gets a sans-serif
     rather than the browser's serif — the exact failure behind the Times "100%". */
  --font-body: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
               "Helvetica Neue", Arial, sans-serif;
  --font-heading: var(--font-body);
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  --font-scale: 1;
  --heading-weight: 700;

  /* The type scale: exactly six sizes for the whole product (owner ruling,
     2026-10-08). Every font-size — and the size inside every font shorthand —
     reads one of these; the app bodies scale theirs by Branding's font scale,
     calc(var(--text-md) * var(--font-scale)). Emails cannot read a variable, so
     src/forx/email/templates.py writes the same px values (and no display size). */
  --text-xs: 11px;       /* captions, counts, uppercase labels */
  --text-sm: 12.5px;     /* secondary text, table cells, controls */
  --text-md: 15px;       /* body */
  --text-lg: 18px;       /* screen and card headings */
  --text-xl: 24px;       /* page titles, headline numbers */
  --text-display: clamp(32px, 4.6vw, 48px);   /* the hero lines */

  --leading-tight: 1.15;
  --leading-body: 1.5;

  /* -------------------------------------------------------- default palette --
     Front Office Rocks brand palette, 2024-25 guide: Dark #000E29 · Bittersweet
     #FF615B · O Supernova #FFB438 · Azalea #F6BFB2. This is the Dark preset the
     console and the member app share; /theme.css overrides it per install, and the
     marketing site redefines what it reads in site.css.

     READ THE GUIDE THE WAY IT IS DRAWN: it is a WHITE document and Dark is its TYPE
     colour. Measured on white, Supernova is 1.77:1, Bittersweet 2.95:1 and Azalea
     1.61:1, so none can carry text; Dark type ON those fills reads 10.81, 6.50 and
     11.89. Brand colour lives in fills; anything that must READ uses --ink or
     --accent-text. */
  --brand-dark: #000E29;
  --brand-coral: #FF615B;
  --brand-amber: #FFB438;

  --bg: #000E29;
  /* The panel colour made opaque. theme.py computes it per theme; this default is
     only what a page renders with no /theme.css. */
  --surface-solid: var(--bg);
  --ink: #f6f7fa;
  --ink-2: rgba(246,247,250,.78);
  /* .52 measured 3.88:1 on white, below AA; .56 clears it. */
  --ink-3: rgba(246,247,250,.56);
  --glass: rgba(0,14,41,.52);
  --glass-2: rgba(255,255,255,.08);
  --glass-hi: rgba(255,255,255,.14);
  --edge: rgba(255,255,255,.14);
  --edge-2: rgba(255,255,255,.26);

  --accent: #FFB438;                /* fills only */
  --accent-ink: #000E29;            /* type ON those fills */
  --accent-soft: rgba(255,180,56,.20);
  /* Supernova is 1.77:1 on white, so it cannot carry text. Every `color:` that wants
     "amber" uses this variant; --accent stays for fills and borders. */
  --accent-text: #FFB438;

  --alert: #FF615B;                 /* Bittersweet darkened until it can carry text */
  /* Alert washes, derived from the theme's alert TEXT colour so they follow it: a theme
     whose alert is a pale pink gets a pale pink wash, not a hardcoded coral one. */
  --alert-soft: color-mix(in srgb, var(--alert) 14%, transparent);
  --alert-edge: color-mix(in srgb, var(--alert) 44%, transparent);
  /* Azalea. `.pill.live`, `.chip.tier` and `.chip.run` once read this while nothing
     defined it, so their colour silently inherited. */
  --warn-text: #F6BFB2;
  /* A status wash laid over a surface: `background: var(--wash-alert), var(--glass)`. */
  --wash-alert: linear-gradient(var(--alert-soft), var(--alert-soft));
  --wash-accent: linear-gradient(var(--accent-soft), var(--accent-soft));
  /* The focus halo around a field. */
  --ring: 0 0 0 3px var(--accent-soft);

  --shadow: 0 2px 6px rgba(0,6,20,.34), 0 18px 48px rgba(0,6,20,.42);
  --logo-h: 28px;

  /* ---------------------------------------------------------------- shape --
     Corners. Literal on purpose (owner ruling, 2026-10-08): corners stay on this fixed
     scale so every install looks the same. Branding's radius slider emits --r-1..4
     (scaled), and only the menu items it already shapes (the nav items) read them; it
     does not reach the rest of the product. */
  --radius-xs: 8px;
  --radius-sm: 10px;
  --radius-md: 12px;
  --radius-lg: 14px;
  --radius-xl: 16px;
  --radius-2xl: 18px;
  --radius-pill: 999px;
  --radius-bar: 99px;

  /* The one border weight. A page whose palette names its line differently (the
     site's --line) redefines this, and every rule written against it follows. */
  --hairline: 1px solid var(--edge);

  /* Frosted glass, by role. Each is written twice in a rule (prefixed and standard). */
  --blur-panel: blur(24px) saturate(150%);
  --blur-card: blur(20px) saturate(150%);
  --blur-chrome: blur(22px) saturate(140%);
  --blur-sheet: blur(26px) saturate(140%);

  /* Stacking order, lowest first. The console's scrim and drawer sit higher (60/70,
     over its sticky table headers); admin.css raises them. */
  --z-backdrop: -2;
  --z-veil: -1;
  --z-sticky: 20;
  --z-topbar: 30;
  --z-scrim: 40;
  --z-drawer: 50;
  --z-menu: 60;
  --z-modal: 70;
  --z-toast: 90;

  /* Motion. */
  --ease-sheet: cubic-bezier(.32,.72,0,1);

  /* ------------------------------------------------------------ components --
     Controls. The member app and the site set these roomier in their own :root; the
     rule that uses them is written once, below. */
  --control-pad-y: var(--space-2);
  --control-pad-x: var(--space-4);
  --control-text: var(--text-sm);

  /* Buttons. One edit here recolours or reshapes every .btn on every surface. */
  --btn-bg: var(--accent);
  --btn-ink: var(--accent-ink);
  --btn-radius: var(--radius-pill);
  --btn-weight: 640;
  --btn-ghost-bg: var(--glass-2);
  --btn-ghost-ink: var(--ink);
  --btn-ghost-hover: var(--glass-hi);
  --btn-danger-bg: var(--alert-soft);
  --btn-danger-ink: var(--alert);
  --btn-danger-edge: var(--alert-edge);

  /* Text fields. Each surface sizes its own (radius, padding, type); the look is here. */
  --field-bg: var(--glass-2);
  --field-border: var(--hairline);
  --field-ink: var(--ink);
  --field-focus: var(--accent);

  /* Panels: the glass card every screen is built from. */
  --panel-bg: var(--glass);
  --panel-border: var(--hairline);
  --panel-radius: var(--radius-2xl);
  --panel-blur: var(--blur-panel);
  --panel-shadow: var(--shadow);
}

/* `hidden` means hidden. Without this, any component that sets its own `display` —
   the member app's plan picker is `display:flex` — ignores the attribute, and a script
   that hides it has silently done nothing. */
[hidden]{ display: none !important; }

/* The page backdrop both app pages paint behind everything. */
#backdrop{
  position: fixed; inset: 0; z-index: var(--z-backdrop);
  background-image: var(--bg-image); background-size: var(--bg-size, cover);
  background-position: center;
}

/* -------------------------------------------------------- container flow --
   The global spacing rule. Applied to the work area of the console and to the
   member app's main column, so every direct child is separated from the one
   before it. Adjacent vertical margins collapse in block layout, so a component
   that still carries its own margin gets max(own, gap), never the sum. */
.work > * + *,
.flow > * + *{
  margin-top: var(--gap-container);
}

/* A section heading belongs to the container under it, so it sits closer to
   that container than to the one above. */
.work > .sectitle + *,
.flow > .sectitle + *{
  margin-top: var(--gap-content);
}

/* The sticky topbar already carries its own padding; the first container under
   it should not be pushed a full container gap further down. */
.work > .topbar + *{
  margin-top: var(--space-2);
}

/* Inside a container: vertical rhythm without per-element margins. */
.stack > * + *{ margin-top: var(--gap-content); }

/* One line of items with a consistent gap. */
.row{
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--gap-inline);
}
.row.baseline{ align-items: baseline; }

/* ------------------------------------------------------------ components --
   ONE definition each, used by both the member app and the console. These used to be
   written twice — once in admin.css, once in index.html — so a change to a button or a
   panel reached one page and not the other. A page may still ADD to a component (the
   console's skeleton sizes, the member lock screen's "or" divider); it does not
   redefine one. tests/test_ui_consistency.py fails if a page defines these again. */

.grow{ flex: 1; }

.panel{
  background: var(--panel-bg);
  border: var(--panel-border);
  border-radius: var(--panel-radius);
  backdrop-filter: var(--panel-blur);
  -webkit-backdrop-filter: var(--panel-blur);
  box-shadow: var(--panel-shadow);
}

.btn{
  display: inline-flex;
  align-items: center;
  gap: var(--gap-inline);
  padding: var(--control-pad-y) var(--control-pad-x);
  border-radius: var(--btn-radius);
  border: 0;
  background: var(--btn-bg);
  color: var(--btn-ink);
  font-weight: var(--btn-weight);
  font-size: var(--control-text);
  cursor: pointer;
  white-space: nowrap;
}
.btn:hover{ filter: brightness(1.06); }
.btn:disabled{ opacity: .42; cursor: not-allowed; filter: none; }
.btn.ghost{ background: var(--btn-ghost-bg); color: var(--btn-ghost-ink); border: var(--hairline); }
.btn.ghost:hover{ background: var(--btn-ghost-hover); }
/* --alert is a text colour; a danger button is alert type on a wash of it. */
.btn.danger{ background: var(--btn-danger-bg); color: var(--btn-danger-ink); border: 1px solid var(--btn-danger-edge); }
.btn.sm{ padding: var(--space-1) var(--space-3); font-size: var(--text-sm); font-weight: 600; }
.btn.link{
  background: none; border: 0; padding: 0;
  color: var(--accent-text);
  font-weight: 620;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* The generated initials block, shown when there is no logo. */
.mark{
  width: 26px; height: 26px; border-radius: var(--radius-xs);
  display: grid; place-items: center; flex: none;
  background: linear-gradient(135deg, var(--brand-coral), var(--brand-amber));
  color: var(--brand-dark);
  font-size: var(--text-sm); font-weight: 800;
}

/* The brand's badge ("operator", or the tagline) and its logo. Both limits on the logo
   are max-*, so a wordmark scales down to whichever binds first and keeps its shape. */
.brand em{
  font-style: normal;
  font-size: var(--text-xs); font-weight: 700; letter-spacing: .11em; text-transform: uppercase;
  color: var(--ink-3);
  border: var(--hairline);
  padding: var(--space-0) var(--space-2); border-radius: 5px;
}
.brand img.logo{ display: block; height: auto; width: auto; max-height: var(--logo-h); }

/* Loading placeholders. Each page sets the shapes it needs. */
.skel{
  border-radius: var(--radius-md);
  background: linear-gradient(90deg, var(--glass-2), var(--glass-hi), var(--glass-2));
  background-size: 200% 100%;
  animation: sh 1.3s infinite;
}
@keyframes sh{ 0%{ background-position: 200% 0; } 100%{ background-position: -200% 0; } }

.empty{ text-align: center; padding: var(--space-8) var(--space-5); color: var(--ink-3); }
.empty .big{ font-size: var(--text-md); font-weight: 600; color: var(--ink); margin-bottom: var(--space-2); }

/* The lock and sign-in screens both pages open on. */
.lock img.logo{
  display: block; height: auto; width: auto;
  max-height: calc(var(--logo-h) * 1.4);
  max-width: min(300px, 100%);
  margin: 0 auto var(--space-3);
}
.lock form{ display: flex; gap: var(--gap-inline); }
.lock input{
  flex: 1;
  background: var(--field-bg);
  border: var(--field-border);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  outline: 0;
  font-size: var(--text-md);
}
.lock input:focus{ border-color: var(--field-focus); }
.lock .bad{ color: var(--alert); font-size: var(--text-sm); margin-top: var(--space-3); min-height: 16px; text-align: center; }


/* A headline number: coverage percentage, a count on a card. Heading family,
   so it follows the Branding screen like every other heading. */
.metric{
  font-family: var(--font-heading);
  font-weight: var(--heading-weight);
  font-size: var(--text-xl);
  line-height: 1;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
.metric.sm{ font-size: var(--text-xl); }

/* Numbers that line up in columns — stat grids, tables, pagers. */
.num{ font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------- menu --
   A dropdown of actions, opened from one button. One element shared by the
   whole page and repositioned on open, rather than one per table row: a library
   page renders fifty rows, and fifty hidden menus is fifty sets of listeners for
   the one that will be used. Colours come from theme tokens, so it follows the
   Branding screen like everything else. */
.menu{
  position: fixed;
  z-index: var(--z-menu);
  min-width: 180px;
  padding: var(--space-1);
  border-radius: var(--radius-md);
  border: var(--hairline);
  background: var(--surface-solid);
  box-shadow: 0 12px 32px rgba(0,0,0,.35);
  display: none;
}
.menu.open{ display: block; }
.menu button{
  display: flex;
  width: 100%;
  align-items: center;
  gap: var(--gap-inline);
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-xs);
  background: none;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.menu button:hover:not(:disabled),
.menu button:focus-visible{ background: var(--glass-2); outline: none; }
.menu button:disabled{ opacity: .45; cursor: not-allowed; }
.menu .hint{
  margin-left: auto;
  font-size: var(--text-xs);
  color: var(--ink-3);
}

/* A table row that opens its detail view when clicked. */
tr.clickable{ cursor: pointer; }
tr.clickable:focus-visible{ outline: 2px solid var(--focus-ring, var(--accent)); outline-offset: -2px; }

/* -------------------------------------------------------------- utilities --
   What the page scripts put on the blocks they build, in place of inline styles, so a
   change to the scale reaches those blocks too. Last in this file so they win over the
   flow rule above; a component whose own page rule sets the same property takes a
   compound rule in that page's sheet instead (it loads after this one). */
.mt-2{ margin-top: var(--space-2); }
.mt-3{ margin-top: var(--space-3); }
.mt-4{ margin-top: var(--space-4); }
.mt-5{ margin-top: var(--space-5); }
.mx-auto{ margin-left: auto; margin-right: auto; }
/* A row of buttons that wraps on a narrow screen. */
.actrow{ display: flex; gap: var(--gap-inline); flex-wrap: wrap; }
/* One centred item, such as "Show more" under a grid. */
.centerrow{ display: flex; justify-content: center; }
