/* Inter, self-hosted, exactly as logistixpert.de ships it - same two files, so
   the product and the site are set in the same face. Self-hosted rather than
   pulled from a CDN: nothing about opening this tool should reach a third
   party, which is the same line the website makes.

   It is a variable font (100-900), which is what the weights below assume.
   Named-but-never-loaded is how this started: the stack said Inter, nothing
   fetched it, and every browser fell through to whatever the OS supplies. */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/static/fonts/inter-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/static/fonts/inter-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Source Serif 4";
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url("/static/fonts/sourceserif4-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Source Serif 4";
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url("/static/fonts/sourceserif4-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  color-scheme: light;

  /* Ground and ink. One set, not two: there used to be a --color-text beside
     --ink and a --color-border beside --line, a percent apart and both in
     use, which is why greys and hairlines drifted from page to page. */
  --bg: #f4f7fb;
  --ink: #142033;
  --muted: #667085;
  --line: #d7e0ea;

  /* The brand: the navy the sidebar is cut from and the green of the mark. */
  --navy: #003f91;
  --navy-deep: #002b63;
  --green: #6abf2b;

  /* Accents the interface leans on, distinct from the brand on purpose. */
  --accent: #0f766e;
  --accent-2: #2563eb;

  /* Semantic, and only semantic — never borrowed for decoration. */
  --danger: #b42318;
  --warning: #f79009;

  /* Spelled out four different ways before this; ids, paths and payloads all
     want the same face. */
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  /* The other half of the brand site's pairing, and it earns its place in one
     job only: the headings of an analysis report. A report is the document
     this tool exists to produce - often the one a customer files as evidence -
     and a serif gives it the register of a record rather than of a screen.
     Never for UI chrome; a control set in a book face reads as a mistake. */
  --serif: "Source Serif 4", Georgia, "Times New Roman", serif;

  /* A scale, because almost everything used to inherit the body's 15px and
     the rest had drifted into 39 different sizes. Labels, dense data, prose,
     headings - four jobs, and a step for each with room either side. */
  --text-2xs: 11px;    /* uppercase labels, chips, badges */
  --text-xs: 12.5px;   /* captions, hints, secondary meta */
  --text-sm: 13.5px;   /* tables, nav, anything dense */
  --text-md: 15px;     /* body copy */
  --text-lg: 17px;     /* subheads */
  --text-xl: 20px;     /* panel headings */
  --text-2xl: 24px;    /* section titles */
  --text-3xl: 32px;    /* a number meant to be read across a room */

  /* Spacing, for the same reason the type has a scale: every gap used to be
     picked by hand, so two panels on one page could sit 6 px and 18 px apart
     with nothing to say why. Four steps and two for the wide ones. */
  --space-1: 4px;      /* inside a line: a label and the value under it */
  --space-2: 8px;      /* between pills, chips, inline controls */
  --space-3: 12px;     /* the floor between any two stacked things */
  --space-4: 16px;     /* between a heading and what it introduces */
  --space-5: 24px;     /* between the parts of a panel */
  --space-6: 32px;     /* between sections of a page */

  --radius-md: 10px;
  --radius-lg: 16px;
  /* Two levels, because an interface only needs two: things that rest on the
     page, and things that float above it. Everything used to sit at 8px/24px,
     so nothing read as nearer than anything else. */
  --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04), 0 1px 3px rgba(15, 23, 42, 0.05);
  --shadow-md: 0 2px 4px rgba(15, 23, 42, 0.05), 0 12px 28px rgba(15, 23, 42, 0.10);
}

* { box-sizing: border-box; }
/* Form controls do not inherit the page font - every browser hands them its
   own default instead. Without this the buttons, selects and inputs stayed on
   the OS face while everything around them moved to Inter, which is the same
   split-personality problem one level down. */
button, input, select, textarea { font-family: inherit; font-size: inherit; }
body {
  margin: 0;
  min-height: 100vh;
  background:
    radial-gradient(circle at top left, rgba(37, 99, 235, 0.12), transparent 34rem),
    radial-gradient(circle at top right, rgba(15, 118, 110, 0.14), transparent 30rem),
    linear-gradient(180deg, #f8fbff 0%, var(--bg) 45%, #eef4f8 100%);
  color: var(--ink);
  /* Spelled out rather than in the `font` shorthand, so the size comes from
     the scale like every other size on the page. */
  font-family: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-size: var(--text-md);
  line-height: 1.5;
}
.topbar {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 14px 28px;
  background: rgba(255, 255, 255, 0.88);
  border-bottom: 1px solid rgba(215, 224, 234, 0.9);
  backdrop-filter: blur(14px);
}
.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-weight: 700;
  color: var(--ink);
  text-decoration: none;
}
.brand-logo {
  display: block;
  width: 288px;
  max-width: 42vw;
  height: auto;
}
.brand-mark {
  display: inline-grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 8px;
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: #fff;
  font-size: var(--text-xs);
  letter-spacing: 0.03em;
}
nav { display: flex; gap: 8px; flex-wrap: wrap; }
nav a {
  color: var(--muted);
  text-decoration: none;
  font-weight: 700;
  padding: 8px 10px;
  border-radius: 7px;
}
nav a:hover { color: var(--accent); background: #edf7f6; }
.nav-form { display: inline-flex; margin: 0; }
.nav-form button {
  padding: 8px 10px;
  background: #eef4fb;
  color: var(--muted);
  box-shadow: none;
}
a { color: var(--accent-2); }
.container { width: min(1280px, calc(100% - 32px)); margin: 30px auto; }
.page-head {
  /* The widest gap on the page: the title and the work under it are two
     different things, and the eye should have to travel. */
  margin-bottom: var(--space-6);
  padding: 6px 0 2px;
}
/* Headings on the scale too. h3 and h4 had no size at all, so they fell back
   to the browser's em multiples - which is how a "Failed Checks" subhead ended
   up at 17.55px, a size nothing else on the page uses. */
h1, h2, h3, h4 { margin: 0 0 var(--space-3); line-height: 1.15; }
h1 { font-size: clamp(30px, 4vw, 46px); letter-spacing: 0; }
h2 { font-size: var(--text-xl); }
h3 { font-size: var(--text-lg); }
h4 { font-size: var(--text-md); }
/* <small> has a browser default of 0.83em, which lands on whatever fraction
   its parent happens to be — the last way a size could still drift. */
small { font-size: var(--text-xs); }

/* The report, and only the report. Its headings carry the brand serif so the
   analysis output reads as a record rather than as a screen — the same
   pairing the website uses. Everything the reader operates rather than reads
   stays in Inter, so the filter chips, buttons and table headers underneath
   are deliberately left out. */
[data-document="report"] h1,
[data-document="report"] h2,
[data-document="report"] h3 {
  font-family: var(--serif);
  font-weight: 600;
  letter-spacing: -0.005em;
}
/* Except the ones that are labels wearing a heading tag — small, uppercase
   and letter-spaced. A book face set that way reads as a mistake. */
[data-document="report"] .run-scope h3 {
  font-family: inherit;
  font-weight: 700;
  letter-spacing: 0.06em;
}
/* And except the inspector's title, which is not a heading at all: it is the
   check id and the field path of whatever is selected, and an identifier
   belongs in the face the rest of the payload is set in. */
[data-document="report"] #viewer-title {
  font-family: var(--mono);
  font-size: var(--text-md);
  font-weight: 700;
  letter-spacing: 0;
}
p { color: var(--muted); }
.metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 14px;
  margin: 18px 0;
}
.auth-shell {
  min-height: calc(100vh - 140px);
  display: grid;
  place-items: center;
}
.auth-card {
  width: min(520px, 100%);
  background: rgba(255, 255, 255, 0.96);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 28px;
  box-shadow: var(--shadow-md);
}
.auth-brand { margin-bottom: 18px; }
.auth-logo {
  display: block;
  width: min(360px, 100%);
  height: auto;
  margin: 0 auto 22px;
}
.auth-card h1 { font-size: var(--text-3xl); }
.metrics div, .panel {
  background: rgba(255, 255, 255, 0.94);
  border: 1px solid rgba(215, 224, 234, 0.95);
  border-radius: 8px;
  padding: 18px;
  box-shadow: var(--shadow-sm);
}
.narrow-panel {
  width: min(520px, 100%);
}
.metrics div {
  position: relative;
  overflow: hidden;
}
.metrics div::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 4px;
  background: linear-gradient(180deg, var(--accent-2), var(--accent));
}
.metrics strong { display: block; font-size: var(--text-3xl); }
.metrics span { color: var(--muted); font-weight: 600; }
/* The verdict card holds a seal, not a number: stack the seal over its label so
   the inline seal and the "Verdict" label cannot collide on one line. */
.metrics div.verdict-metric { display: flex; flex-direction: column; justify-content: center; gap: 6px; }
.metrics div.verdict-metric .verdict-seal { font-size: var(--text-xl); }
/* Choices to run an eye down, one per line: versions, message kinds. A row of
   chips reads as a sentence and hides how many there are. */
.tick-list {
  display: grid;
  gap: 4px;
  margin-top: .3rem;
}
.tick-item {
  display: flex;
  align-items: center;
  gap: .45rem;
  font-size: var(--text-sm);
}
.tick-item small { color: var(--muted); margin-left: .25rem; }

/* A decision with consequences, after several files were uploaded at once.
   Deliberately not .upload-panel and not .tick-list: the first is a two-column
   grid for a picker beside a button, which lands one option per column, and
   the second is a compact one-line-per-tick list with no room to say what a
   choice means. Here each option is a line of its own with its consequence
   spelled out underneath, because the two answers store different things. */
.batch-decide { display: grid; gap: 16px; margin-top: 14px; }
.choice-list { display: grid; gap: 12px; border: 0; padding: 0; margin: 0; min-width: 0; }
.choice-list > .form-group-title { padding: 0; margin: 0 0 .2rem; }
.choice { display: grid; grid-template-columns: auto 1fr; gap: .6rem; align-items: start; cursor: pointer; }
.choice input[type="radio"] { margin: .3rem 0 0; }
.choice-text strong { display: block; }
.choice-text small { display: block; color: var(--muted); }
.choice-field { display: grid; gap: .3rem; max-width: 34rem; }
.choice-field small { color: var(--muted); }

/* The project picker sits in a table cell beside plain text, so it reads as
   part of the row rather than as a control dropped into it: a step
   under the cell's own type, and a width it cannot grow out of. A project
   named after a customer and a hall is longer than the column can ever be,
   so it is cut rather than allowed to push the table sideways. */
td select[name="project_id"] {
  font-size: var(--text-2xs);
  /* No width:100% - a select with no intrinsic width lets the column collapse
     to the width of its header. It sizes to its longest option instead, with
     a floor so it stays clickable and a ceiling so one long project name
     cannot push the row's actions off the page. */
  min-width: 7rem;
  max-width: 14rem;
  text-overflow: ellipsis;
  padding: 4px 6px;
}
/* The offer that stands in for the picker while a workspace has no project:
   two words that must not break over two lines in a narrow column. */
.project-empty { white-space: nowrap; }
/* Created and Owner: which run it is matters, when and by whom is context.
   Set to the same step as the project picker beside them, so the three read
   as one band of secondary information rather than three competing ones. */
.cell-meta { font-size: var(--text-2xs); color: var(--muted); white-space: nowrap; }
/* The four severity counts in one column. They were four, each as wide as its
   own heading rather than as the digit beneath it, and together they took a
   sixth of a table that scrolled on a 27-inch screen. Kept on one line: the
   row is read across, and a count that wrapped under its neighbour would be
   read as belonging to the next column. */
.sev-cell { white-space: nowrap; }
/* The spacing sits on a wrapper rather than on the pills: a rule of its own on
   `.severity-count` would be the first one in this file, and the house check
   that every pill declares `white-space: nowrap` reads the first it finds. */
.sev-group { display: inline-flex; gap: 3px; align-items: center; }

/* The account page: tiles that are links where there is something to open, and
   plain where the number is the whole story. A folded panel keeps the same
   frame as an open one, so the page does not jump about as boxes are opened. */
/* Tiles that are links where there is something to open. Every tile rule in
   this file targets `.metrics div`, so a link got no frame and no accent stripe
   and stood beside its neighbours as bare text - which is exactly how the
   Analyses tile looked. The frame is repeated here rather than widened above,
   because widening `.metrics div` to `.metrics a` would reach every page that
   ever puts a link in a tile. */
.account-metrics a {
  text-decoration: none;
  color: inherit;
  position: relative;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.94);
  border: 1px solid rgba(215, 224, 234, 0.95);
  border-radius: 8px;
  padding: 18px;
  box-shadow: var(--shadow-sm);
  display: block;
}
.account-metrics a::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 4px;
  background: linear-gradient(180deg, var(--accent-2), var(--accent));
}
.account-metrics a:hover strong { color: var(--accent); }
/* And a step down from the dashboard's number-across-a-room: these are counts
   you read while sitting at the page, not from the other side of a hall.
   Written with both classes because a later block in this file sets
   `.metrics strong` to clamp(24px, 3vw, 34px) - equal specificity, so it wins
   on position alone, and a rule that depends on where it sits is a rule that
   breaks the next time the file is reordered. */
.metrics.account-metrics strong { font-size: var(--text-xl); }
.metrics.account-metrics span { font-size: var(--text-xs); }
/* The same stripe the tiles beside it carry, from that same later block. */
.account-metrics a::before { width: 5px; background: linear-gradient(180deg, var(--navy), var(--green)); }
.metrics.account-metrics a { min-height: 0; }
.account-findings { margin: 12px 0 0; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
details.panel > summary {
  cursor: pointer;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px;
}
details.panel > summary strong { font-size: var(--text-md); }
details.panel[open] > summary { margin-bottom: 12px; }

/* The check selection on the analyze page, folded because everything relevant
   is already ticked. Narrowed, it says so in its own frame. */
.check-panel > summary { cursor: pointer; }
.check-panel-narrowed > summary strong { color: var(--accent); }
/* Message kinds, comma-separated: several fit on one line, many wrap inside
   the cell instead of stretching it. */
/* Wide enough for two or three pills a line: at 12rem each kind took a line
   of its own and a six-type capture made the row six lines tall. */
.kind-cell { white-space: normal; min-width: 20rem; max-width: 26rem; }
/* An imported-at column is read as a moment, not compared character by
   character - but it must not wrap into two lines while doing it. */
td.stamp { white-space: nowrap; }

/* A third for the verdict, two for the audit trail beside it. Below the
   breakpoint the two stack, like every other pair on the page. */
.grid.split-third {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: 18px;
  align-items: start;
}
@media (max-width: 900px) {
  .grid.split-third { grid-template-columns: 1fr; }
}
/* A hash has no spaces to break at, so the value column is told to break
   anywhere rather than push the panel wider than its share of the row. Scoped
   to that one column on purpose: the same rule on every cell once turned
   "Severity" into "Seve rity". */
.audit-table td + td { overflow-wrap: anywhere; }
.grid.two, .form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 18px;
  align-items: start;
}
/* A report is read top to bottom, and its panels are not comparable pairs:
   a scorecard table next to a three-item risk list leaves the short one
   trailing half a page of white. One box per row, each as wide as the page. */
[data-document="report"] .grid.two {
  grid-template-columns: 1fr;
}
/* The verdicts are the one deliberate exception: with custom rules a run has
   the standard's verdict and the custom one, and two cards of the same shape
   side by side are exactly the comparison the page owes the reader. Without
   custom rules the single card keeps the same half of the page, not a
   banner's width for one word. */
.verdict-cards {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 18px;
  align-items: start;
}
/* The Layout verdict and the partner's answer are four lines each; Check
   Coverage is a table of thirteen rows. As three cells of the grid that put a
   short card beside the long table and a column of white space under it. The
   two short ones share one cell instead, stacked, and the table takes the cell
   beside them.

   Stacking them is deliberately not the same as spanning Check Coverage over
   two rows: a spanning tile makes the grid divide its height across the rows
   it covers, and the two short cards drift apart by the difference. In one
   cell they simply sit on top of each other. */
.verdict-stack { display: flex; flex-direction: column; gap: 18px; min-width: 0; }
@media (max-width: 1100px) {
  .verdict-cards { grid-template-columns: 1fr; }
}
.verdict-card h2 { margin-bottom: 2px; }
.verdict-card .hint { margin: 0 0 12px; }
.verdict-top { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin: 2px 0 12px; }
.verdict-pill, .score-pill {
  font-size: var(--text-xl);
  font-weight: 700;
  letter-spacing: 0.02em;
  padding: 5px 18px;
  border-radius: 999px;
}
.verdict-pass { color: #166534; background: #dcfce7; }
.verdict-warning { color: #92400e; background: #fef3c7; }
.verdict-fail { color: #991b1b; background: #fee2e2; }
.verdict-none { color: var(--muted); background: #eef2f7; }
.score-pill { color: #12356b; background: #e6edf7; }
/* Coverage: the same shape as the score pill, tinted by class, because the
   two are read as a pair. A run can be green on one and pale on the other -
   that contrast is the whole point of showing them side by side. */
.coverage-pill {
  font-size: var(--text-xl);
  font-weight: 700;
  letter-spacing: 0.02em;
  padding: 5px 18px;
  border-radius: 999px;
  text-decoration: none;
  color: #12356b;
  background: #e6edf7;
}
.coverage-pill span { font-size: var(--text-sm); font-weight: 600; }
.coverage-sample { color: #991b1b; background: #fee2e2; }
.coverage-partial { color: #92400e; background: #fef3c7; }
.coverage-broad { color: #12356b; background: #e6edf7; }
.coverage-certification { color: #166534; background: #dcfce7; }
.coverage-gates { list-style: none; margin: 6px 0 0; padding: 0; display: grid; gap: 4px; }
.coverage-gates li { font-size: var(--text-sm); padding-left: 22px; position: relative; }
.coverage-gates li::before { position: absolute; left: 0; font-weight: 700; }
.gate-met { color: var(--muted); }
.gate-met::before { content: "\2713"; color: #166534; }
.gate-open::before { content: "\2013"; color: #92400e; }
.gate-holders { color: var(--muted); }
.coverage-gaps { margin-top: 14px; }
.coverage-gaps summary { font-weight: 600; cursor: pointer; }
.coverage-gaps ul { margin: 8px 0 0; padding-left: 18px; display: grid; gap: 8px; }
.coverage-gaps li strong { display: block; }
.coverage-gaps li span { color: var(--muted); font-size: var(--text-sm); }
/* The fleet's errors as one list: the filter sits in the table header, where
   a reader looks for it, rather than above the card. */
.perf-error-filter { display: flex; align-items: center; gap: 8px; }
.perf-error-filter label { font-size: var(--text-sm); color: var(--muted); }
.perf-error-filter select { min-width: 12rem; }
/* The ruleset file, formatted: long enough to need its own scroll, and read
   rather than skimmed - so it keeps the monospace column width of the message
   views instead of stretching across the page. */
/* ============================== Landing page ============================= */
/* The first page for somebody who is not signed in. It shares the palette and
   the type of the application - a visitor should recognise the product they
   land in - but none of its chrome: no sidebar, no workspace banner, nothing
   that belongs to a session that does not exist yet. */
.landing { max-width: 68rem; margin: 0 auto; padding: var(--space-6) var(--space-4) var(--space-6); }
/* The hero band of logistixpert.de, brought across as it is built there: the
   same photograph at the same 22% over the site's own ink, under the same two
   gradients - a 104-degree wash and a blue glow out of the top left corner.
   Somebody who arrives from the product page should recognise the ground they
   are standing on before they read a word.

   Only the band, not the page: the site is dark at the top and light beneath
   it, and so is this. */
.landing-hero {
  text-align: center;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: #0c1524;
  color: #eef1f6;
  padding: clamp(40px, 5vw, 88px) clamp(20px, 4vw, 56px);
  margin-bottom: var(--space-6);
}
.landing-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  background-image: url("/static/landing-bg.jpg");
  background-size: cover;
  background-position: center 46%;
  opacity: 0.22;
  filter: saturate(0.35);
}
.landing-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(104deg, #0c1524 0%, rgba(12, 21, 36, 0.94) 42%,
                    rgba(12, 21, 36, 0.68) 72%, rgba(12, 21, 36, 0.8) 100%),
    radial-gradient(120% 90% at 8% 0%, rgba(42, 74, 155, 0.42) 0%, transparent 62%);
}
/* On the dark band the type is light, and the note under the buttons keeps its
   quieter step without falling into the background it now sits on. */
.landing-hero h1 { color: #ffffff; }
.landing-hero .landing-claim { color: rgba(238, 241, 246, 0.86); }
.landing-hero .landing-note { color: rgba(238, 241, 246, 0.66); }

/* Edge to edge, like the band on logistixpert.de. The page gets a container of
   its own rather than breaking out of the narrow one with `100vw`: that trick
   counts the scrollbar as page width and hands you a horizontal scrollbar on
   every machine that shows one. What is constrained is the content, not the
   band - which is how the site does it too. */
/* Both written with the class they have to beat: a later `.container` rule in
   this file sets 1480px, and `.landing` its own 68rem, so equal specificity
   would leave the band at whatever those two agreed on - which is exactly
   where it stayed on the first attempt. */
.container.landing-container { width: 100%; max-width: none; margin: 0 0 30px; padding: 0; }
.landing-container > .landing { max-width: none; margin: 0; padding: 0 0 var(--space-6); }
/* Sign-in and registration's own band, edge to edge like the landing page's:
   same photo, same opacity, same two gradients. A visitor who just left the
   landing page for either should not land on a plain white page in between. */
.container.auth-hero-container { width: 100%; max-width: none; margin: 0; padding: 0; }
.auth-shell--branded {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: #0c1524;
  padding: clamp(40px, 5vw, 88px) clamp(20px, 4vw, 56px);
}
.auth-shell--branded::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  background-image: url("/static/landing-bg.jpg");
  background-size: cover;
  background-position: center 46%;
  opacity: 0.22;
  filter: saturate(0.35);
}
.auth-shell--branded::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(104deg, #0c1524 0%, rgba(12, 21, 36, 0.94) 42%,
                    rgba(12, 21, 36, 0.68) 72%, rgba(12, 21, 36, 0.8) 100%),
    radial-gradient(120% 90% at 8% 0%, rgba(42, 74, 155, 0.42) 0%, transparent 62%);
}
.landing-hero h1,
.landing-hero .landing-claim,
.landing-hero .landing-actions,
.landing-hero .landing-note { max-width: 860px; margin-inline: auto; }
/* Everything under the band keeps the page's own measure. */
.landing-facts,
.landing-foot { width: min(1280px, calc(100% - 32px)); margin-inline: auto; }
/* The product page's hero, wider than a wordmark and carrying its own frame:
   it holds four screenshots, so it needs the room to be read as pictures
   rather than as a smudge, and a rounded edge keeps it from butting into the
   heading beneath. Capped at 72rem - the same measure the product page's own
   wide wrap holds its hero to - since it is the same picture and should read
   at the same size a visitor just saw it at. */
.landing-hero-art {
  width: min(72rem, 100%);
  height: auto;
  margin: 0 auto var(--space-5);
  display: block;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}
.landing-hero h1 {
  /* The product page sets every heading, this one included, in the serif -
     Inter is its body face, not its voice. This is the one heading here that
     borrows it: not a UI label the reader operates, but the same claim the
     product page makes, echoed rather than paraphrased in a different face.
     600 rather than the product page's own 550 - this stylesheet keeps to
     four weights and 600 is already what every other heading here carries,
     close enough that nobody will see the half-step. Tracking and the size
     curve (--step-4) still match its own h1 exactly. */
  font-family: var(--serif);
  font-size: clamp(2rem, 1.58rem + 2.03vw, 3.15rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin-bottom: var(--space-4);
}
/* The one word the product page's own h1 breaks colour on. Its own green,
   not this application's - the hero photo above already carries that exact
   shade in the logo, and the two should not disagree. */
.landing-hero-accent { color: #8ac23a; }
.landing-claim {
  max-width: 44rem;
  margin: 0 auto var(--space-5);
  font-size: var(--text-lg);
  line-height: 1.6;
  color: var(--ink);
}
.landing-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); justify-content: center; }
.landing-note { margin-top: var(--space-3); font-size: var(--text-sm); color: var(--muted); }
.landing-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--space-4);
  margin-top: var(--space-6);
}
.landing-facts article {
  background: rgba(255, 255, 255, 0.96);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--space-5);
}
.landing-facts h2 { font-size: var(--text-lg); color: var(--navy-deep); }
.landing-facts p { margin: 0; color: var(--muted); line-height: 1.6; }
.landing-foot {
  margin-top: var(--space-6);
  padding-top: var(--space-5);
  border-top: 1px solid var(--line);
  text-align: center;
  font-size: var(--text-sm);
  color: var(--muted);
}
.landing-foot p + p { margin-top: var(--space-2); }

/* Coverage in a list, beside the score: same shape as the score badge so the
   pair reads as a pair, and the class carries the colour - a high score next
   to a pale coverage is exactly the contrast the column is there to show. */
/* An action in a table header keeps to one line: the header is a flex row and
   a two-word button was wrapping into "Add a / member". */
.table-header .page-actions .button,
.table-header .page-actions button { white-space: nowrap; }

.coverage-badge {
  display: inline-block;
  min-width: 3.2rem;
  text-align: center;
  padding: 2px 8px;
  border-radius: 999px;
  font-weight: 700;
  font-size: var(--text-sm);
  color: #12356b;
  background: #e6edf7;
}
.coverage-badge.coverage-sample { color: #991b1b; background: #fee2e2; }
.coverage-badge.coverage-partial { color: #92400e; background: #fef3c7; }
.coverage-badge.coverage-broad { color: #12356b; background: #e6edf7; }
.coverage-badge.coverage-certification { color: #166534; background: #dcfce7; }

/* Whether a ruleset agrees with the standard, as one glyph in its row. The
   colour carries the meaning and the tooltip carries the sentence; the count
   beside it says how many rules are involved. */
.rs-flag { display: inline-flex; align-items: center; gap: var(--space-1); text-decoration: none; }
/* The glyph takes the colour of the state, like every other icon in the
   app: without an explicit fill an inline SVG paints itself black and the
   difference between "agrees" and "contradicts" is a shape nobody reads. */
.rs-flag svg { width: 18px; height: 18px; fill: currentColor; }
.rs-flag span { font-size: var(--text-xs); font-weight: 700; }
.rs-flag-ok { color: #166534; }
.rs-flag-note { color: var(--warning); }
.rs-flag-bad { color: var(--danger); }
.rs-flag-bad:hover, .rs-flag-note:hover { text-decoration: underline; }
/* The JSON of a ruleset or a requirement file. It carried `color: var(--ink)`,
   which is #142033 - and a class beats an element selector, so it won that
   against the light text `pre` sets for its near-black background. The result
   was dark navy on near-black: legible only if you already knew what it said.
   The colour is `pre`'s again. */
.ruleset-json {
  margin: 0;
  max-height: 70vh;
  overflow: auto;
  font-family: var(--mono);
  font-size: var(--text-sm);
  line-height: 1.5;
  white-space: pre;
}
/* The verdict as an inspection seal. Colour rides on the container as
   currentColor, so rim, glyph and a tinted face all follow one declaration, and
   the label may wrap instead of forcing the box open. */
.verdict-seal { display: inline-flex; align-items: center; gap: 8px; max-width: 100%; font-weight: 700; font-size: var(--text-md); line-height: 1.15; }
.verdict-seal .seal-label { white-space: normal; overflow-wrap: break-word; }
/* Two characters of air after the word, so a tinted cell around the seal does
   not stop hard against the last letter. */
.verdict-seal { padding-right: 0.6em; }
.seal-icon { width: 1.9em; height: 1.9em; flex: 0 0 auto; }
.seal-rim, .seal-mark { stroke: currentColor; }
.seal-face { fill: color-mix(in srgb, currentColor 14%, white); }
.seal-mark-fill { fill: currentColor; }
.verdict-seal.verdict-pass { color: #166534; }
.verdict-seal.verdict-pass_with_warnings, .verdict-seal.verdict-warning { color: #92400e; }
.verdict-seal.verdict-fail { color: #991b1b; }
.verdict-seal.verdict-not_applicable, .verdict-seal.verdict-none { color: #64748b; }
.verdict-severities { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 4px; }
.verdict-card .table-scroll { margin-top: 12px; }
.verdict-scores td:last-child { text-align: right; font-weight: 700; color: #12356b; }
/* The compare page: movement is the message, so the deltas carry the colour
   and the unchanged tail steps back. */
.delta-worse { color: #991b1b; font-weight: 700; }
.delta-better { color: #166534; font-weight: 700; }
.compare-unchanged td { color: var(--muted); }
/* What became of a finding. "Open" is the absence of a chip on purpose. */
.dispo-chip {
  font-size: var(--text-2xs);
  font-weight: 700;
  padding: 3px 10px;
  border-radius: 999px;
  border: 1px solid transparent;
  background: #f1f5f9;
  color: #475569;
  cursor: pointer;
  white-space: nowrap;
}
.dispo-chip.dispo-accepted { background: #dcfce7; color: #166534; }
.dispo-chip.dispo-false_positive { background: #e2e8f0; color: #334155; }
.dispo-chip.dispo-to_fix { background: #fef3c7; color: #92400e; }
.dispo-chip.dispo-open { background: #fff; border-color: var(--line); color: #94a3b8; }
/* Covered by a standing waiver rather than decided here: same colours, dashed
   edge - a rule about the kind, not a decision about the finding. */
.dispo-chip.waived { border-style: dashed; border-color: currentColor; }
/* The dialog is native; only its box is ours. */
.dispo-dialog { border: 0; border-radius: var(--radius-lg); box-shadow: var(--shadow-md); padding: 0; width: min(460px, 92vw); }
.dispo-dialog::backdrop { background: rgba(15, 23, 42, 0.45); }
.dispo-dialog form { padding: 20px; display: grid; gap: 12px; }
.dispo-dialog h3 { margin: 0; }
.dispo-dialog .dispo-finding { font-family: var(--mono); font-size: var(--text-xs); color: var(--muted); overflow-wrap: break-word; }
.dispo-dialog textarea { min-height: 84px; resize: vertical; }
.dispo-dialog .actions { margin: 0; justify-content: flex-end; }

/* The coverage explanation. Wider than a disposition dialog because it is
   prose about seven figures rather than one field to fill in, and scrollable
   because on a short screen it would otherwise run off the bottom. */
.coverage-help { width: min(680px, 94vw); }
.coverage-help-body { padding: 22px 24px 20px; max-height: 82vh; overflow-y: auto; }
.coverage-help-body h3 { margin: 0 0 6px; }
.coverage-help-body h4 { margin: 18px 0 4px; font-size: var(--text-sm); }
.coverage-help-body p { margin: 0 0 4px; line-height: 1.55; }
.coverage-help-body code { font-family: var(--mono); font-size: var(--text-sm); }
.coverage-help-body .actions { margin-top: 18px; justify-content: flex-end; }

/* A button that reads as a link: it opens an explanation rather than changing
   anything, and a second solid button beside a heading would claim more of the
   eye than the heading. */
.linkish {
  background: none;
  border: 0;
  padding: 0;
  color: var(--accent);
  font: inherit;
  font-size: var(--text-sm);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
  white-space: nowrap;
}
.linkish:hover { color: var(--ink); }

/* Where a custom ruleset and the standard disagree. A hard conflict is a rule
   to rewrite and gets the weight of one; the softer two are decisions to
   confirm, not faults, and must not shout at somebody whose plant standard is
   simply narrower than VDA5050. */
.rs-conflict {
  border-left: 3px solid var(--line);
  border-radius: 0 8px 8px 0;
  padding: 10px 14px;
  margin-top: 10px;
  background: #f8fafc;
}
.rs-conflict-hard { border-left-color: #b91c1c; background: #fef2f2; }
.rs-conflict-soft { border-left-color: #94a3b8; }
.rs-conflict-head { display: flex; gap: 8px; align-items: center; margin-bottom: 4px; flex-wrap: wrap; }
.rs-conflict strong { display: block; }
.rs-conflict p { margin: 3px 0 0; font-size: var(--text-sm); }
.rs-conflict code { font-family: var(--mono); font-size: var(--text-xs); overflow-wrap: break-word; }
/* The same statement repeated at the rule itself. `rs-conflict` is the hard
   kind and shares its name with the block class above, so the inline form has
   to undo the block's border and background. */
.rs-inline-conflict { margin: 6px 0 0; font-size: var(--text-sm); color: var(--muted); }
.rs-inline-conflict.rs-conflict {
  border: 0;
  padding: 0;
  background: none;
  color: #b91c1c;
}
.rs-inline-conflict strong { display: inline; }
.standard-ok { color: var(--muted); }

/* The verdict on a file the moment it is taken in. Loud enough not to be
   scrolled past, quiet enough not to read as a refusal - the upload succeeded,
   and a deliberate deviation is the customer's to make. */
.upload-review { border-left: 4px solid #94a3b8; }
.upload-review-hard { border-left-color: #b91c1c; }
.upload-review h2 { margin: 0 0 6px; font-size: var(--text-md); }
.upload-review p { margin: 0 0 8px; }
.upload-review-list { margin: 0 0 8px; padding-left: 18px; }
.upload-review-list li { font-size: var(--text-sm); margin-bottom: 3px; }
.upload-review-list code { font-family: var(--mono); font-size: var(--text-xs); }
.dispo-meta { font-size: var(--text-2xs); color: var(--muted); }
/* The compare picker: one row per candidate run, the whole row the link. */
.compare-pick { padding: 20px; display: grid; gap: 12px; }
.compare-pick h3 { margin: 0; }
.compare-pick .hint { margin: 0; }
.compare-choices { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; max-height: 320px; overflow-y: auto; }
.compare-choices a {
  display: flex;
  gap: 12px;
  align-items: baseline;
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-radius: 8px;
  text-decoration: none;
  color: inherit;
}
.compare-choices a:hover { border-color: var(--accent-2); background: #f6fafd; }
.compare-pick .actions { margin: 0; justify-content: flex-end; }
/* The carry offer holds a form: sentence left, button right, one row. */
.carry-offer { display: flex; align-items: center; gap: 16px; }
.carry-offer span { flex: 1; }
.carry-offer .inline-form { flex: 0 0 auto; }
/* And a guaranteed gap between the boxes, so nothing on a report ever sits
   flush against the box above it. */
[data-document="report"] > section {
  margin-bottom: 18px;
}
table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 8px 24px rgba(20, 32, 51, 0.05);
}
/* A table narrower than its content does not wrap, it clips - and clipped
   text reads as text that was never there. The fix is to let the content
   shrink, not to let the table grow: the `breakable` filter marks the joints
   in an identifier so a check id or a field path stops holding a whole column
   open. `width: 100%` on a table is a floor rather than a ceiling, so anything
   that still cannot fit widens the table and the .table-scroll wrapper
   (overflow-x: auto, further down) scrolls it instead of clipping. Sizing the
   table to max-content instead, as this briefly did, stops the sentence
   columns wrapping at all and sends a nine-column findings table straight off
   the side of the screen. */
.table-scroll > table { width: 100%; }
/* Lining figures, because these tables are read down their columns: message
   counts, four severity counts and a score. Proportional digits make those
   columns ripple - a 1 is not the width of a 0 - and a table nobody can scan
   vertically is not doing its job. */
th, td { text-align: left; padding: 11px 12px; border-bottom: 1px solid #edf1f5; vertical-align: top;
         font-variant-numeric: tabular-nums; overflow-wrap: break-word; }
/* A column label may wrap between its two words - "Raised by" over two lines
   is fine, and holding it on one costs the sentence columns their room - but
   never inside one. Mid-word is what turned "Severity" into "Seve rity" and
   "Message" into three lines. */
th { overflow-wrap: normal; }
/* An identifier has no spaces, so a narrow column has nowhere to wrap it and
   the column holds itself open. The break opportunities come from the
   `breakable` filter, which marks the joints - after a separator, and at the
   humps of camelCase - so lastNodeSequenceId wraps as lastNode/SequenceId
   rather than wherever the line happened to run out. */
.ident { font-family: var(--mono); font-size: var(--text-xs); line-height: 1.45; }
th { background: #edf4fb; font-weight: 700; color: #344054; }
tr:last-child td { border-bottom: 0; }
.truncate {
  max-width: 260px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A file name with the capture profile it came out of underneath. Recordings
   are listed newest first rather than in a block per profile, so the row has to
   carry its own provenance. The name keeps the single-line ellipsis; only the
   cell around it is allowed to grow to two lines. */
.source-cell { max-width: 280px; }
.source-cell .truncate { display: block; max-width: 100%; }
.source-cell small { display: block; }
.severity-count {
  display: inline-flex;
  white-space: nowrap;
  min-width: 28px;
  justify-content: center;
  padding: 3px 8px;
  border-radius: 999px;
  font-weight: 700;
  font-size: var(--text-xs);
}
.severity-critical { color: #991b1b; background: #fee2e2; }
.severity-major { color: #9a3412; background: #ffedd5; }
.severity-minor { color: #854d0e; background: #fef3c7; }
.severity-warning { color: #92400e; background: #fde68a; }
.firmness-tag {
  display: inline-block;
  padding: 1px 6px;
  border-radius: 999px;
  border: 1px solid #cbd5e1;
  color: #475569;
  background: #f8fafc;
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: 0.03em;
  vertical-align: middle;
  white-space: nowrap;
}
.severity-info { color: #1d4ed8; background: #dbeafe; }
.finding-row { transition: background 0.15s ease; }
.finding-row:hover, .finding-row.is-selected { background: #eefaf7; }
form { display: grid; gap: 14px; }
label { display: grid; gap: 6px; font-weight: 700; color: #344054; }
label.inline, .toggle-row { display: flex; align-items: center; gap: 8px; }
.toggle-row {
  align-self: end;
  min-height: 43px;
  padding: 10px 12px;
  background: #f6fafc;
  border: 1px solid var(--line);
  border-radius: 8px;
}
input, select {
  width: 100%;
  padding: 11px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #fff;
  color: var(--ink);
  font: inherit;
}
input:focus, select:focus {
  outline: 3px solid rgba(37, 99, 235, 0.15);
  border-color: var(--accent-2);
}
/* A placeholder is an example, not a value. Set in the same weight and colour
   as the text it sits in, it reads as a field that is already filled - people
   submit the form and wonder why the twin is called AMR-500. Paler and italic
   says "this is what one looks like" at a glance, before anybody clicks. */
input::placeholder, textarea::placeholder {
  color: #9aa5b6;
  font-style: italic;
  opacity: 1;                       /* Firefox dims placeholders again */
}
/* The sentence under a field that explains what was put in it, or what will be.
   Quiet enough not to compete with the label above. */
.field-note {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--text-2xs);
  font-style: italic;
  color: var(--muted);
  font-weight: 400;
}
/* Both, not only the checkbox. `input { width: 100% }` above is right for a
   text field and wrong for a tick: a radio that had not been exempted took the
   whole row in the export dialog and pushed its own label out of the way. */
input[type="checkbox"], input[type="radio"] { width: auto; accent-color: var(--accent); }
button, .button {
  display: inline-block;
  width: fit-content;
  /* Transparent, not none. .secondary and .soft-danger draw a real border and
     the rest draw none, so side by side in a row of actions the solid ones
     came out 2px shorter and sat a pixel low. The border a button does not
     draw still has to take up the room. .row-actions used to say this for
     itself; it is true of every row, so it is said here. */
  border: 1px solid transparent;
  border-radius: 8px;
  /* An <a class="button"> inherits the page's line-height and a <button> takes
     the user agent's "normal" one, which is four pixels of difference for the
     same label. Every row that mixes the two - and every list does, because
     "Open" is a link and "Delete" is a form's submit - stepped up and down
     across its own controls. One line-height, one height. */
  line-height: 1.2;
  padding: 10px 14px;
  background: linear-gradient(135deg, var(--accent), #14958b);
  color: #fff;
  text-decoration: none;
  /* A control label is dense UI, not prose. Left to inherit the body size it
     came out a step larger than it had been under the browser default, and a
     row of six page actions started wrapping onto three lines. */
  font-size: var(--text-sm);
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 8px 18px rgba(15, 118, 110, 0.18);
}
button:hover, .button:hover { filter: brightness(0.98); transform: translateY(-1px); }
.button.compact {
  padding: 7px 10px;
  font-size: var(--text-xs);
  white-space: nowrap;
}
button:disabled, .button:disabled {
  cursor: not-allowed;
  filter: grayscale(0.35);
  opacity: 0.55;
  transform: none;
}
.button.danger {
  background: linear-gradient(135deg, var(--danger), #e11d48);
  box-shadow: 0 8px 18px rgba(180, 35, 24, 0.16);
}
.inline-form {
  display: inline-block;
  margin-left: 8px;
}
.inline-form button { margin: 0; }
/* Every name this application has for "a row of controls" ends up here, so a
   template that reaches for one of them gets the floor without asking. A row
   that touches is the rule in docs/styleguide.md broken in the place people
   notice first, and tests/test_nothing_touches.py fails the day it happens. */
.actions, .action-row, .btn-row {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  align-items: center;
  margin: 16px 0;
}
.filter-panel {
  display: flex;
  gap: 12px;
  align-items: end;
  flex-wrap: wrap;
  margin-bottom: 18px;
}
.filter-panel label {
  min-width: min(360px, 100%);
}
.muted {
  color: var(--muted);
  font-size: var(--text-xs);
}
.admin-actions {
  display: grid;
  gap: 8px;
  min-width: 280px;
}
.admin-actions form {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}
.admin-actions input {
  width: min(180px, 100%);
  padding: 8px 10px;
}
.filter-bar {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  /* Clear of the rule above it, so the chips are not glued to the heading. */
  margin: 10px 0 12px;
}
.filter-chip {
  background: #ffffff;
  color: var(--accent);
  border: 1px solid var(--line);
  padding: 7px 10px;
  font-size: var(--text-xs);
  box-shadow: none;
}
.filter-chip.is-active {
  background: linear-gradient(135deg, var(--accent-2), var(--accent));
  color: #ffffff;
  border-color: transparent;
}
.settings-toolbar {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  color: var(--muted);
  padding-top: 6px;
}
.settings-toolbar strong { color: var(--ink); }
.manual-layout {
  display: grid;
  grid-template-columns: minmax(210px, 260px) 1fr;
  gap: 18px;
  align-items: start;
}
/* Twenty-five entries for an administrator, and it has to stand still while
   the manual scrolls past it - so it is set as tight as it can be read at:
   no gap between the rows, the padding carrying the whole rhythm, and the
   dense type step the tables use. The max-height is the safety net rather
   than the plan: on a short screen it scrolls itself instead of pushing the
   page. */
.manual-toc {
  position: sticky;
  top: 104px;
  display: grid;
  gap: 0;
  max-height: calc(100vh - 124px);
  overflow-y: auto;
}
/* Both written with `.panel` alongside, because a later block in this file sets
   the panel padding and the link margin, and equal specificity would leave the
   outcome to whichever line happens to come last. The 12px margin every link
   in a panel carries was the whole difference between a 23px row and a 35px
   one - twenty-one entries of it is a quarter of a screen. */
.panel.manual-toc { padding: 12px; }
.panel.manual-toc a { margin: 0; }
.manual-toc strong {
  color: var(--ink);
  margin-bottom: 6px;
}
.manual-toc a {
  text-decoration: none;
  font-weight: 600;
  font-size: var(--text-sm);
  line-height: 1.25;
  color: var(--muted);
  padding: 3px 7px;
  border-radius: 6px;
}
.manual-toc a:hover {
  color: var(--accent);
  background: #edf7f6;
}
.manual-content {
  display: grid;
  gap: 16px;
}
.manual-section {
  scroll-margin-top: 110px;
}
.manual-section ul,
.manual-section ol {
  margin: 0;
  padding-left: 22px;
  color: var(--muted);
}
.manual-section li { margin: 6px 0; }
.manual-section code {
  padding: 2px 5px;
  border-radius: 5px;
  background: #eef4fb;
  color: #1d4ed8;
}
.policy-list {
  display: grid;
  grid-template-columns: minmax(150px, 220px) 1fr;
  gap: 8px 16px;
  color: var(--muted);
}
.policy-list dt {
  font-weight: 700;
  color: #344054;
}
.policy-list dd {
  margin: 0;
}
.check-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 14px;
}
.check-grid.compact { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.check-group {
  margin: 0;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 12px;
  background: linear-gradient(180deg, #ffffff, #f8fbfd);
}
.check-group legend { padding: 0 6px; font-weight: 700; color: var(--accent); }
.check-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 10px;
  align-items: start;
  padding: 8px 0;
  border-bottom: 1px solid #edf1f5;
  font-weight: 500;
}
.check-row:last-child { border-bottom: 0; }
.check-row input { width: auto; margin-top: 3px; }
.check-row label { display: block; font-weight: 500; }
.check-row small { display: block; color: var(--muted); font-size: var(--text-xs); }
.info-dot {
  position: relative;
  display: inline-grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border-radius: 999px;
  background: #e7f3ff;
  color: var(--accent-2);
  box-shadow: none;
  font-size: var(--text-xs);
  line-height: 1;
}
.info-dot:hover,
.info-dot:focus {
  transform: none;
  filter: none;
  background: var(--accent-2);
  color: #fff;
}
.info-dot::after {
  content: attr(data-tooltip);
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  z-index: 20;
  width: min(340px, 80vw);
  padding: 10px 12px;
  border-radius: 8px;
  background: #101828;
  color: #fff;
  box-shadow: var(--shadow-md);
  font-weight: 500;
  line-height: 1.45;
  text-align: left;
  white-space: normal;
  opacity: 0;
  pointer-events: none;
  transform: translateY(-4px);
  transition: opacity 0.12s ease, transform 0.12s ease;
}
.info-dot:hover::after,
.info-dot:focus::after {
  opacity: 1;
  transform: translateY(0);
}
/* Once tooltip.js is running the same text is drawn in the layer at the end of
   <body>, and drawing it here as well would show every trigger two. */
.tips-in-a-layer .info-dot::after { content: none; }

/* ------------------------------------------------------------ tip layer --
   Every tooltip on the page, drawn here rather than where it belongs.
   In place they were cut off by whatever ancestor happened to clip - a panel
   with rounded corners and overflow:hidden, a table that scrolls sideways, a
   card shorter than the text hanging out of it. An element cannot escape an
   ancestor's clipping box however high its z-index, so the cure is not to be
   inside one: fixed, a child of <body>, clipped by nothing. */
.tip-layer {
  position: fixed;
  z-index: 9000;
  width: max-content;
  max-width: min(46ch, 90vw);
  max-height: 70vh;
  overflow-y: auto;
  padding: 12px 14px;
  border-radius: 10px;
  background: #101828;
  color: #fff;
  box-shadow: 0 12px 32px rgba(16, 24, 40, 0.28);
  font-size: var(--text-sm);
  font-weight: 500;
  line-height: 1.45;
  text-align: left;
  pointer-events: none;
}
.tip-layer[hidden] { display: none; }
/* A tooltip may be several lines - one per enterprise a report went to - and
   the layer sets textContent, so the newlines have to survive. */
.tip-layer { white-space: pre-line; }
.tip-layer strong { display: block; margin-top: 10px; color: #fff; }
.tip-layer strong:first-child { margin-top: 0; }
.tip-layer ul { margin: 4px 0 0; padding-left: 18px; }
.tip-layer li { color: #e6eaf2; }
.tip-layer em { display: block; margin-top: 10px; color: #b6c2d6; font-style: normal; }
.upload-panel { display: grid; gap: 14px; grid-template-columns: 1fr auto; align-items: end; }
.file-picker {
  position: relative;
  display: block;
  width: 100%;
  padding: 12px;
  border: 1px dashed #9eb3c7;
  border-radius: 8px;
  background: #f8fbfd;
  color: var(--muted);
  cursor: pointer;
}
.file-picker input {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}
.error { color: var(--danger); font-weight: 700; }
/* A warning that has to be read before the button under it is pressed.
   The testbench's live run is the one place in the product where a click
   moves a machine, and it should not look like every other paragraph. */
.panel.warn { border-left: 4px solid #d97706; background: #fffbeb; }
.success { color: #177245; }
.hint {
  /* It used to carry a -4px top margin, which is how a hint came to sit on
     the line above it wherever it followed anything but a heading. Under a
     heading it still wants to be close, and that is said where it is true. */
  margin: 0 0 2px;
  font-size: var(--text-xs);
}
/* In a table cell a hint is a second line under the value, not a continuation
   of it. As an inline span it ran straight on: "JohnDoe2026-08-16 00:39". */
td > .hint {
  display: block;
  margin: 2px 0 0;
}
:is(h1, h2, h3, h4, .field-label, .eyebrow) + .hint { margin-top: calc(-1 * var(--space-1)); }
.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  place-items: center;
  padding: 24px;
  background: rgba(16, 24, 40, 0.52);
}
.modal-backdrop[hidden] { display: none; }
.modal-card {
  width: min(1100px, 100%);
  max-height: min(86vh, 920px);
  overflow: hidden;
  display: grid;
  grid-template-rows: auto 1fr;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 24px 70px rgba(16, 24, 40, 0.24);
}
.modal-head {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  align-items: start;
  padding: 18px 20px;
  border-bottom: 1px solid var(--line);
}
.modal-head h2 { margin-bottom: 4px; }
.modal-head p { margin: 0; }
/* Scope switch in the context modal: which neighbourhood is shown. It sits
   between the finding and the panes it governs, centred over them. */
.context-scope { justify-content: center; margin: 14px 0 2px; }
.context-scope .filter-chip:disabled { opacity: 0.45; cursor: default; }
.modal-close {
  width: 34px;
  height: 34px;
  padding: 0;
  border-radius: 999px;
  background: #eef4fb;
  color: var(--ink);
  box-shadow: none;
  font-size: var(--text-2xl);
  line-height: 1;
}
.context-stack {
  display: grid;
  gap: 14px;
  overflow: auto;
  padding: 18px 20px 20px;
}
.context-message h3 {
  margin: 0 0 4px;
  font-size: var(--text-md);
  color: var(--accent);
}
/* Which vehicle a context column belongs to — so a neighbouring column that is
   a different vehicle reads as one, not as the same stream. */
.context-vehicle { margin: 0 0 8px; font-size: var(--text-xs); font-weight: 600; color: var(--muted); font-family: var(--mono); }
/* Previous, current and next side by side: a reader compares fields across
   the three, and a stack forces scrolling between every comparison. Below the
   width where three columns still fit they fall back to one. */
[data-context-messages] {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
  align-items: stretch;
}
[data-context-messages].is-single { grid-template-columns: minmax(0, 1fr); }
/* Three JSON columns need the room; the other dialogs stay as they were. */
.modal-card.context-card { width: min(1600px, 100%); }
.context-message { display: flex; flex-direction: column; min-width: 0; }
.context-message pre {
  flex: 1;
  max-height: min(58vh, 620px);
  overflow: auto;
  margin: 0;
}
.context-message.is-current h3 { color: var(--navy-deep); }
@media (max-width: 1100px) {
  [data-context-messages] { grid-template-columns: minmax(0, 1fr); }
}
.context-finding {
  border: 1px solid var(--line);
  border-left-width: 4px;
  border-radius: 8px;
  padding: 10px 14px;
  font-size: var(--text-sm);
  background: #f8fafc;
}
.context-finding .cf-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; }
.context-finding .cf-head code { background: rgba(15, 23, 42, 0.06); border-radius: 5px; padding: 2px 7px; font-size: var(--text-xs); }
.context-finding p { margin: 0; }
.context-finding .cf-rec { margin-top: 4px; color: #475569; }
.context-finding.sev-critical { border-left-color: #dc2626; background: #fef2f2; }
.context-finding.sev-major { border-left-color: #ea580c; background: #fff7ed; }
.context-finding.sev-minor { border-left-color: #ca8a04; background: #fefce8; }
.context-finding.sev-info { border-left-color: #2563eb; background: #eff6ff; }
.ctx-line { display: block; }

/* MQTT capture profiles */
.profile-form summary {
  cursor: pointer;
  font-weight: 700;
  color: var(--ink);
  font-size: var(--text-lg);
}
.profile-form[open] summary { margin-bottom: 14px; }
.profile-card { margin-top: 14px; }
/* The reports are a different subject from the live boxes above them, so the
   gap is wider than the 14px that separates one profile from the next. */
.dashboard-reports { margin-top: 28px; }
.profile-head { display: flex; justify-content: space-between; gap: 16px; align-items: start; flex-wrap: wrap; }
.profile-title h2 { margin-bottom: 4px; display: flex; align-items: center; gap: 10px; }
.profile-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.profile-actions form { margin: 0; }
.run-badge {
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: 0.6px;
  color: #fff;
  background: #16a34a;
  border-radius: 999px;
  padding: 3px 10px;
}

/* A report signed by someone else says so at the top of the page. */
.brand-strip {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0 0 14px;
  padding: 8px 14px;
  border-left: 4px solid #12356b;
  background: #f8fafc;
  border-radius: 6px;
  font-weight: 700;
}

.brand-strip img { max-height: 34px; }

/* A report that arrived from a partner says so at the top, and their covering
   note is set apart from our own prose — the reader has to be able to tell
   whose words they are. */
.shared-from { margin: 0 0 18px; }

.share-note {
  margin: 12px 0 0;
  padding: 2px 0 2px 14px;
  border-left: 3px solid var(--line, #d9dee6);
  white-space: pre-wrap;
}

.share-pick { display: grid; gap: 12px; max-width: 34rem; }

.share-pick textarea { width: 100%; }

/* What is left of a capture profile's day. A number the reader acts on, so it
   sits on the card next to Start rather than behind a click — and it changes
   colour only when it is about to matter. */
.day-budget {
  align-self: center;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--muted, #667085);
  white-space: nowrap;
}

.day-budget.low { color: #92400e; }

.day-budget.spent { color: #991b1b; }

/* A connector that is not recording is not idle, it is down. */
.run-badge.down { background: #b91c1c; }

.interruptions { margin-top: 10px; }

.interruptions > summary { cursor: pointer; font-weight: 600; }

.interruptions > summary .muted { font-weight: 400; }

/* The licence speaks up only when it changes what someone can do today. */
.license-banner {
  margin: 0 0 18px;
  padding: 10px 14px;
  border-radius: 8px;
  font-size: var(--text-md);
  border: 1px solid #fcd34d;
  background: #fffbeb;
  color: #92400e;
}

.license-banner.expired {
  border-color: #fca5a5;
  background: #fef2f2;
  color: #991b1b;
}

/* A report a partner handed over is news, not a warning: the same shape as the
   licence banner so it sits in the same place, in a colour that does not say
   something is wrong. */
.license-banner.shared-report {
  border-color: #bfdbfe;
  background: #eff6ff;
  color: #1e3a8a;
}

.license-banner.shared-report .muted { color: #3b6bb5; margin-left: .4em; }

.license-banner a { font-weight: 700; }

.license-status {
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: 0.6px;
  border-radius: 999px;
  padding: 3px 12px;
  color: #fff;
  background: #16a34a;
}

.license-status.grace { background: #d97706; }
.license-status.expired { background: #dc2626; }

.feature-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
.feature-list .tick { font-weight: 700; }
.feature-list .tick.yes { color: #16a34a; }
.feature-list .tick.no { color: #94a3b8; }
tr.row-over td { background: #fef2f2; }

/* An encrypted connection says so in the profile line; the trailing "!" marks
   the case where the certificate is deliberately not verified. */
.tls-badge {
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: 0.5px;
  color: #0f766e;
  background: #ccfbf1;
  border: 1px solid #99f6e4;
  border-radius: 999px;
  padding: 1px 7px;
}
.live-chip {
  font-size: var(--text-xs);
  color: #334155;
  background: #f1f5f9;
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 5px 12px;
}
.runs-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); margin-top: 10px; }
.runs-table th, .runs-table td { text-align: left; padding: 6px 10px; border-bottom: 1px solid #eef2f7; }
.runs-table .run-active { background: #f0fdf4; }
.run-row-actions { display: flex; gap: var(--space-2); align-items: center; }
.run-row-actions form { margin: 0; }

/* MQTT subscriber stream */
.sub-stream {
  max-height: 560px;
  min-height: 260px;
  overflow: auto;
  font-size: var(--text-xs);
  line-height: 1.5;
}
.sub-stream .st-head { display: block; color: #7dd3fc; font-weight: 700; }
.sub-stream .st-body { display: block; color: #e2e8f0; }
.success-note { color: #166534; background: #f0fdf4; border: 1px solid #bbf7d0; }

/* MQTT subscriber message inspector */
.inspector { display: grid; grid-template-columns: minmax(340px, 5fr) 6fr; gap: 16px; align-items: start; }
.inspector-detail { display: grid; gap: 16px; }
.msg-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 660px;
  overflow-y: auto;
  padding-right: 4px;
}
/* Grouped findings render as rows, not as spaced cards. */
.msg-list:has(.fg) { gap: 0; border: 1px solid var(--line); border-radius: 8px; }
.msg-item {
  display: grid;
  grid-template-columns: 10px 1fr auto;
  gap: 10px;
  align-items: center;
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #fff;
  box-shadow: none;
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: inherit;
}
.msg-item:hover { background: #f8fafc; }
.msg-item.is-selected { border-color: #2563eb; box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.18); }
.msg-item .mi-dot { width: 10px; height: 10px; border-radius: 50%; background: #94a3b8; }
.msg-item .mi-main { min-width: 0; display: block; }
.msg-item .mi-topic {
  display: block;
  font-weight: 600;
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.msg-item .mi-meta { display: block; font-size: var(--text-2xs); color: var(--muted); }
.msg-item .mi-meta code { font-size: var(--text-2xs); }
.msg-item .mi-badge { font-size: var(--text-xs); font-weight: 700; color: #15803d; }
.msg-item.is-ok .mi-dot { background: #16a34a; }
.msg-item.is-ok { border-left: 3px solid #16a34a; }
.msg-item.sev-critical .mi-dot, .msg-item.sev-major .mi-dot { background: #dc2626; }
.msg-item.sev-critical { border-left: 3px solid #b91c1c; background: #fef2f2; }
.msg-item.sev-major { border-left: 3px solid #dc2626; background: #fef2f2; }
.msg-item.sev-minor { border-left: 3px solid #ca8a04; background: #fefce8; }
.msg-item.sev-minor .mi-dot { background: #ca8a04; }
.msg-item.sev-info { border-left: 3px solid #2563eb; background: #eff6ff; }
.msg-item.sev-info .mi-dot { background: #2563eb; }
.msg-item.sev-critical .mi-badge, .msg-item.sev-major .mi-badge { color: #b91c1c; }
.msg-item.sev-minor .mi-badge { color: #a16207; }
.msg-item.sev-info .mi-badge { color: #1d4ed8; }
.ri-list-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
.ri-modes { display: flex; gap: 6px; }
.ri-modes .is-active { background: #2563eb; color: #fff; border-color: #2563eb; }
#ri-filters { margin: 10px 0 6px; }
#ri-filters .filter-chip span { opacity: 0.7; font-variant-numeric: tabular-nums; }
#ri-msg-list .mi-meta { white-space: normal; }
#sub-detail-findings, #ri-detail-findings { display: grid; gap: 10px; max-height: 260px; overflow-y: auto; }
.ok-note {
  margin: 0;
  padding: 10px 14px;
  border: 1px solid #bbf7d0;
  border-left: 4px solid #16a34a;
  border-radius: 8px;
  background: #f0fdf4;
  color: #166534;
  font-size: var(--text-sm);
}
@media (max-width: 1100px) {
  .inspector { grid-template-columns: 1fr; }
  .msg-list { max-height: 320px; }
}
.sub-h3 { margin: 14px 0 8px; font-size: var(--text-md); }
.compact-metrics { margin: 0 0 6px; }
.compact-metrics div strong { font-size: var(--text-2xl); }
.ctx-line.is-hl {
  background: rgba(220, 38, 38, 0.28);
  box-shadow: inset 3px 0 0 #ef4444;
  border-radius: 3px;
}
pre {
  max-height: 460px;
  overflow: auto;
  padding: 12px;
  background: #101828;
  color: #eff6ff;
  border-radius: 8px;
  font-size: var(--text-xs);
  line-height: 1.55;
}
@media (max-width: 760px) {
  .topbar { align-items: flex-start; flex-direction: column; gap: 10px; }
  .manual-layout { grid-template-columns: 1fr; }
  .manual-toc { position: static; }
  .policy-list { grid-template-columns: 1fr; }
  .upload-panel { grid-template-columns: 1fr; }
  th, td { padding: 9px; }
}

/* SaaS app shell refresh */
body {
  background:
    radial-gradient(circle at 16% 0%, rgba(0, 63, 145, 0.12), transparent 30rem),
    radial-gradient(circle at 88% 10%, rgba(106, 191, 43, 0.12), transparent 26rem),
    linear-gradient(135deg, #f8fbff 0%, var(--bg) 55%, #eef6fb 100%);
  color: var(--ink);
}
.app-shell {
  min-height: 100vh;
  display: grid;
  grid-template-columns: 281px minmax(0, 1fr);
}
.sidebar {
  position: sticky;
  top: 0;
  height: 100vh;
  /* 5px at the top: the logo sits that close to the edge. */
  padding: 5px 14px 18px;
  /* The whole column sits on the site render that opens the landing page,
     dimmed under navy until it is texture rather than picture - the same
     recipe as the hero, seen through the sidebar's own gradient. */
  background:
    linear-gradient(180deg, rgba(4, 31, 72, 0.84), rgba(6, 60, 128, 0.86) 58%, rgba(5, 47, 103, 0.9)),
    url("/static/landing-bg.jpg") center 46% / cover no-repeat #052f67;
  color: #fff;
  box-shadow: 14px 0 36px rgba(0, 43, 99, 0.16);
  overflow-y: auto;
}
.sidebar-brand {
  display: block;
  /* Reaches 9px past the column's 14px padding: 5px from either edge, like
     the top. */
  margin: 0 -9px 10px;
  padding: 0 0 8px;
  text-align: center;
  border-bottom: 1px solid rgba(255, 255, 255, 0.14);
}
.sidebar .brand-logo {
  display: block;
  width: 100%;
  padding: 6px;
  border-radius: var(--radius-md);
  background: #fff;
}
.side-nav {
  display: grid;
  gap: 1px;
  /* A step below body size: navigation is scanned, not read, and it should
     not compete with the page it points at. */
  font-size: var(--text-sm);
}
.nav-section {
  margin: 14px 8px 4px;
  color: rgba(255, 255, 255, 0.92);
  font-size: var(--text-md);
  font-weight: 700;
  letter-spacing: 0.02em;
}
/* The emblem from the logo - the green and blue arcs around the glass - sits
   before every top-level heading: the same mark that opens the sidebar, at
   the size of the text it introduces. */
.nav-section::before {
  content: "";
  display: inline-block;
  width: 22px;
  height: 22px;
  margin-right: 9px;
  vertical-align: -5px;
  background: url("/static/nav-mark.png") center / contain no-repeat;
}
/* Weight carries the hierarchy rather than shouting it: every item at 500,
   and the page you are on is the one bold thing in the column. */
.side-nav a {
  color: rgba(255, 255, 255, 0.82);
  text-decoration: none;
  font-weight: 500;
  letter-spacing: 0.004em;
  /* Second level starts where the heading's text starts: past the emblem
     (8px margin + 22px mark + 9px gap = 39px, with the link's own 11px). */
  margin-left: 28px;
  /* Tightened by about a third: the column is scanned, not read. */
  padding: 5.5px 11px;
  border-radius: var(--radius-md);
  transition: background 0.16s ease, color 0.16s ease, transform 0.16s ease;
}
.side-nav a:hover,
.side-nav a.active {
  color: #fff;
  background: rgba(255, 255, 255, 0.14);
  transform: translateX(2px);
}
.side-nav a.active {
  font-weight: 600;
  box-shadow: inset 3px 0 0 var(--green);
}
.app-content {
  min-width: 0;
}
.topbar {
  position: sticky;
  top: 0;
  display: flex;
  justify-content: flex-end;
  gap: 14px;
  min-height: 51px;
  padding: 6px 28px;
  /* The sidebar's top edge runs on across the page: a band exactly as tall
     as the logo's gap to the top. */
  border-top: 5px solid var(--navy-deep);
  background: rgba(255, 255, 255, 0.82);
  border-bottom: 1px solid rgba(216, 226, 239, 0.86);
  backdrop-filter: blur(18px);
}
.topbar-status,
.user-area {
  display: flex;
  align-items: center;
  gap: 10px;
}
.topbar-status {
  margin-right: auto;
  color: var(--muted);
  font-weight: 600;
}
.status-dot {
  width: 10px;
  height: 10px;
  border-radius: 999px;
  background: var(--green);
  box-shadow: 0 0 0 4px rgba(106, 191, 43, 0.16);
}
/* The dot names the instance at a glance: prod green, test blue, dev red. */
.status-dot--test { background: #2563eb; box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.16); }
.status-dot--dev { background: #dc2626; box-shadow: 0 0 0 4px rgba(220, 38, 38, 0.16); }
/* Amber for the demonstration instance: not the red of a workbench, because
   what it runs was released to test, and not the green of production, because
   what happens on it is not a customer's live work. */
.status-dot--demo { background: #d97706; box-shadow: 0 0 0 4px rgba(217, 119, 6, 0.16); }
.user-pill {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 6px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: #fff;
  color: var(--navy-deep);
  font-weight: 700;
}
.container {
  width: min(1480px, calc(100% - 48px));
  margin: 28px auto 48px;
}
.auth-container {
  width: min(760px, calc(100% - 32px));
}
.page-head {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  align-items: end;
  margin-bottom: 22px;
}
.page-head h1 {
  font-size: clamp(28px, 3vw, 40px);
  color: var(--navy-deep);
}
.page-head p {
  max-width: 820px;
  margin: 0;
}
.page-actions {
  display: flex;
  /* 12px floor between boxes, same as everywhere else. */
  gap: 12px;
  flex-wrap: wrap;
  align-items: center;
}
/* The one action that belongs at the far end of the row. */
.page-actions .push-right { margin-left: auto; }
.panel,
.metrics div,
.card,
.table-card,
.quick-card {
  background: rgba(255, 255, 255, 0.96);
  border: 1px solid rgba(216, 226, 239, 0.96);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}
.panel,
.card,
.table-card,
.quick-card {
  padding: 20px;
}
.card-header,
.table-header,
.section-header {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  align-items: center;
  margin-bottom: var(--space-4);
}
.card-header p,
.table-header p,
.section-header p {
  margin: 3px 0 0;
}

/* A section that starts folded. Used for the parts of a monitoring page that
   are read when a fleet is set up and rarely again - the rules it is judged by,
   what its machines actually send. Open, they stood between the reader and the
   readings they came for; gone, nobody could check them. Folded is the answer
   to both, and <details> is the one element that needs no script to do it. */
.fold > summary {
  cursor: pointer;
  list-style: none;
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border: 1px solid rgba(216, 226, 239, 0.96);
  border-radius: var(--radius-lg);
  /* The same ground a .panel stands on - a folded section is a panel that
     happens to be shut, and a second white would show as a seam. */
  background: rgba(255, 255, 255, 0.96);
  box-shadow: var(--shadow-sm);
}
.fold > summary::-webkit-details-marker { display: none; }
/* The marker is drawn rather than inherited, so it turns with the section and
   sits at a size that matches the heading beside it. */
.fold > summary::before {
  content: "";
  width: 0; height: 0; flex: 0 0 auto;
  border-left: 6px solid var(--muted);
  border-top: 5px solid transparent; border-bottom: 5px solid transparent;
  transition: transform .12s ease;
}
.fold[open] > summary::before { transform: rotate(90deg); }
.fold > summary:hover { border-color: var(--accent); }
.fold > summary h2 { margin: 0; }
.fold[open] > summary { margin-bottom: var(--space-4); }
.metrics {
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--space-5);
}
.metrics div {
  min-height: 104px;
  padding: 18px;
}
.metrics div::before {
  width: 5px;
  background: linear-gradient(180deg, var(--navy), var(--green));
}
.metrics strong {
  color: var(--navy-deep);
  font-size: clamp(24px, 3vw, 34px);
}
.metrics span {
  color: var(--muted);
}
button,
.button {
  border-radius: var(--radius-md);
  background: linear-gradient(135deg, var(--navy), #0d5ec4);
  box-shadow: 0 10px 22px rgba(0, 63, 145, 0.18);
}
.button.secondary,
button.secondary {
  border: 1px solid var(--line);
  background: #fff;
  color: var(--navy);
  box-shadow: var(--shadow-sm);
}
.button.accent,
button.accent {
  background: linear-gradient(135deg, #5aa826, var(--green));
  color: #fff;
}
.button.danger,
button.danger {
  /* Light stop to dark, the way every other button is built — the two reds
     that used to be two tokens are one token and its highlight. */
  background: linear-gradient(135deg, #d92d20, var(--danger));
}
/* Between secondary and danger: an action that takes something away and gives
   it back again. Suspending an enterprise carried this class for months with
   nothing defined behind it, so the one button that locks out a whole company
   looked exactly like Save. */
.button.warn,
button.warn {
  background: linear-gradient(135deg, #f5a524, #d97706);
  box-shadow: 0 8px 18px rgba(217, 119, 6, 0.18);
}
/* A tint of red, not a red button: the way to report a problem should stand
   out from the page's ordinary actions without shouting like the one that
   deletes something. */
.button.soft-danger {
  background: #fdecec;
  color: var(--danger);
  border: 1px solid rgba(180, 35, 24, 0.28);
  box-shadow: none;
}
.button.soft-danger:hover {
  background: #fbdcdc;
  color: #8f1c13;
}
button:focus-visible,
.button:focus-visible,
a:focus-visible,
input:focus,
select:focus {
  outline: 3px solid rgba(47, 128, 237, 0.2);
  outline-offset: 2px;
}
.table-card {
  padding: 0;
  overflow: hidden;
}
.table-card > .table-header.panel {
  margin: 0;
  border-width: 0 0 1px;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  box-shadow: none;
  background: #fff;
}
.table-card .filter-bar {
  padding: 0 20px 14px;
}
.table-scroll {
  overflow-x: auto;
}
table {
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: #f0f6ff;
  color: var(--navy-deep);
}
tr:hover td {
  background: #f9fcff;
}
.badge,
.score-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 26px;
  padding: 4px 9px;
  border-radius: 999px;
  font-weight: 700;
  font-size: var(--text-xs);
}
.score-good { color: #166534; background: #dcfce7; }
.score-warn { color: #92400e; background: #fef3c7; }
.score-bad { color: #991b1b; background: #fee2e2; }
.severity-count {
  min-height: 26px;
  border-radius: 999px;
}
.empty-state {
  display: grid;
  place-items: center;
  gap: 10px;
  min-height: 180px;
  padding: 28px;
  text-align: center;
  color: var(--muted);
  border: 1px dashed var(--line);
  border-radius: var(--radius-lg);
  background: rgba(255, 255, 255, 0.72);
}
.quick-actions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 16px;
  margin: 18px 0;
}
.quick-card {
  display: grid;
  gap: 10px;
  color: inherit;
  text-decoration: none;
}
.quick-card strong {
  color: var(--navy-deep);
  font-size: var(--text-lg);
}
.quick-card span {
  color: var(--muted);
}
.quick-card:hover {
  transform: translateY(-2px);
  border-color: rgba(0, 63, 145, 0.26);
}
.form-grid {
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
/* The setup panel asks two questions: what is analyzed, and what it is
   measured against. The second row holds pickers of unequal height — a list
   plus a line of explanation next to a plain select — so its cells align at
   the top instead of stretching to the tallest one. Stretching is what left
   the empty half-screen under the first four fields. */
/* Only the reference row needs this: its cells are of unequal height — a list
   plus a line of explanation next to a plain select — and stretching them to
   the tallest is what left the empty half-screen. Both rows hold exactly three
   fields and nothing full-width, so auto-fit collapses to the same three
   columns in each and they line up. */
.reference-grid { align-items: start; }
.form-group-title {
  margin: 20px 0 10px;
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: #64748b;
}
.form-group-title span {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
  color: #94a3b8;
  margin-left: 8px;
}
/* Picking several of a handful of things: a checkbox list rather than a
   multi-select. Every option is visible without being discovered, and taking
   two of three needs no modifier key. Scrolls at six rows so a long shelf of
   factsheets cannot push the run button off the screen. */
/* auto-fill rather than auto-fit: the capture profile's run row holds one
   field when the account has no projects, and a lone select stretched across
   the whole panel reads as a mistake. Keeping the empty tracks also lines the
   row up with the three references underneath it. */
.run-grid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
/* The analysis setup is one question with two halves: which file, and what is
   in it. The file gets the wider half - it carries a name, a message count and
   a kind on one line - and the two tick lists share the other. Below 60em the
   halves stack, which is the same order read down a phone. */
@media (min-width: 60em) {
  .analysis-setup.run-grid { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); align-items: start; }
}
.capture-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--space-4); align-items: start; }
.field { display: grid; gap: 6px; }
.field-label { font-weight: 700; color: #344054; }
.pick-list {
  border: 1px solid #d0d5dd;
  border-radius: 8px;
  background: #fff;
  max-height: 168px;
  overflow-y: auto;
}
.pick-item {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: 7px 10px;
  border-top: 1px solid #eef2f7;
  font-weight: 400;
  cursor: pointer;
}
.pick-item:first-child { border-top: none; }
.pick-item:hover { background: #f8fafc; }
.pick-item input { width: auto; margin: 2px 0 0; flex: none; }
.pick-text { display: grid; gap: 1px; min-width: 0; }
/* Same treatment as the tables: the names in a 240px cell are file names and
   vehicle ids, and they wrap at their joints rather than wherever the line
   ran out. */
.pick-text strong { font-size: var(--text-md); color: #16233a; overflow-wrap: break-word; }
.pick-text small { color: #64748b; overflow-wrap: break-word; }
/* A factsheet or ruleset written for another target version is not a choice,
   so the list does not offer it. The empty note below says why a list is
   bare, which is what a greyed-out row used to be for. */
.pick-item[hidden] { display: none; }
.pick-empty {
  margin: 0;
  padding: 12px;
  border: 1px dashed #d0d5dd;
  border-radius: 8px;
  color: #64748b;
  font-size: var(--text-sm);
}
.pick-list > .pick-empty { border: none; padding: 10px 12px; }

/* The manufacturer switch is a modifier on the factsheet choice above it, so
   it reads as one sentence with its reason rather than as a fourth field. */
.inline-check.stacked { align-items: flex-start; margin-top: 12px; }
.inline-check.stacked input { margin-top: 3px; }
.inline-check.stacked small { display: block; font-weight: 400; margin-top: 2px; max-width: 90ch; }
.form-section {
  display: grid;
  gap: 14px;
  padding: 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: #fbfdff;
}
.form-section h2,
.form-section h3 {
  margin-bottom: 2px;
}
.helper-text,
small {
  color: var(--muted);
  font-weight: 500;
}
input,
select {
  border-radius: var(--radius-md);
}
input[type="password"] {
  background-color: #fff !important;
}
.upload-panel {
  display: grid;
  gap: 14px;
  grid-template-columns: 1fr;
}
.file-picker {
  display: grid;
  place-items: center;
  min-height: 168px;
  padding: 24px;
  border: 2px dashed #9eb9da;
  border-radius: var(--radius-lg);
  background: linear-gradient(180deg, #ffffff, #f4f9ff);
  text-align: center;
}
.file-picker strong {
  display: block;
  color: var(--navy-deep);
  font-size: var(--text-lg);
}
.code-card pre,
pre {
  border: 1px solid #243145;
  border-radius: var(--radius-md);
}
.check-grid,
.check-grid.compact {
  grid-template-columns: 1fr;
}
.check-group {
  border-radius: var(--radius-lg);
  background: #fff;
  padding: 0;
  overflow: hidden;
}
.check-group legend {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  align-items: center;
  width: calc(100% + 2px);
  margin: -1px -1px 0;
  padding: 14px 16px;
  color: var(--navy-deep);
  background: #f4f8ff;
  border-bottom: 1px solid var(--line);
}
.check-group legend::after {
  content: "";
  display: table;
  clear: both;
}
.check-group .check-row {
  padding: 12px 16px;
}
.check-tools {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.analysis-footer {
  position: sticky;
  bottom: 0;
  z-index: 4;
  display: flex;
  justify-content: flex-end;
  padding: 14px 0 0;
  background: linear-gradient(180deg, rgba(245, 249, 255, 0), var(--bg) 46%);
}
.status-card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 14px;
  margin: 16px 0;
}
.status-card {
  padding: 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: #fff;
  box-shadow: var(--shadow-sm);
}
.status-card strong {
  display: block;
  margin-top: 4px;
  color: var(--navy-deep);
  font-size: var(--text-xl);
}
.status-card span {
  color: var(--muted);
  font-weight: 600;
}
.menu-button,
.nav-toggle {
  display: none;
}
@media (max-width: 980px) {
  .app-shell {
    grid-template-columns: 1fr;
  }
  .sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: 30;
    width: min(280px, 86vw);
    transform: translateX(-105%);
    transition: transform 0.2s ease;
  }
  .nav-toggle:checked ~ .sidebar {
    transform: translateX(0);
  }
  .menu-button {
    display: inline-flex;
    align-items: center;
    min-height: 36px;
    padding: 8px 11px;
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: #fff;
    color: var(--navy);
    font-weight: 700;
    cursor: pointer;
  }
  .topbar {
    justify-content: space-between;
    padding: 12px 16px;
  }
  .topbar-status {
    display: none;
  }
  .container {
    width: min(100% - 24px, 100%);
    margin-top: 20px;
  }
  .page-head {
    align-items: start;
    flex-direction: column;
  }
}
@media (max-width: 640px) {
  .metrics,
  .quick-actions,
  .status-card-grid {
    grid-template-columns: 1fr;
  }
  .user-area {
    gap: 6px;
  }
  .user-pill {
    max-width: 140px;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  th,
  td {
    white-space: nowrap;
  }
}

/* Final app-shell guards: keep legacy global nav/card styles from leaking in. */
body .app-shell {
  display: grid;
  grid-template-columns: 281px minmax(0, 1fr);
}
body .app-shell .sidebar {
  display: block;
}
body .app-shell .side-nav {
  display: grid;
  flex-wrap: initial;
  gap: 3px;
}
body .app-shell .side-nav a,
body .app-shell .nav-section {
  display: block;
}
body .app-shell .sidebar .brand-logo {
  width: 100%;
}
body .app-shell .nav-toggle {
  display: none;
}
body .app-shell .quick-actions {
  display: grid;
}
body .app-shell .quick-card {
  display: grid;
  text-decoration: none;
}
@media (max-width: 980px) {
  body .app-shell {
    grid-template-columns: 1fr;
  }
  body .app-shell .sidebar {
    position: fixed;
  }
}

/* ---------- Detailed findings page ---------- */
.detail-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 18px;
  align-items: start;
}
.detail-list { display: grid; gap: 16px; min-width: 0; }
.detail-group > h2 { font-size: var(--text-md); margin: 0 0 8px; color: #475569; }
.detail-check {
  border: 1px solid #e2e8f0;
  border-radius: 10px;
  background: #fff;
  overflow: hidden;
  margin-bottom: 8px;
}
.detail-check-head {
  display: grid;
  /* severity · family · title · count */
  grid-template-columns: auto auto 1fr auto;
  gap: 10px;
  align-items: center;
  width: 100%;
  padding: 10px 12px;
  background: #f8fafc;
  border: 0;
  cursor: pointer;
  text-align: left;
}
.detail-check-head:hover { background: #f1f5f9; }
.detail-check-title { display: grid; min-width: 0; }
.detail-check-title code { font-size: var(--text-xs); color: #0f172a; }
.detail-check-title small { color: #64748b; }
.detail-count {
  background: #e2e8f0;
  border-radius: 999px;
  padding: 2px 9px;
  font-size: var(--text-xs);
  color: #334155;
}
.detail-check-body { padding: 10px 12px; border-top: 1px solid #e2e8f0; }
.detail-desc { margin: 0 0 6px; font-size: var(--text-sm); color: #334155; }
.spec-ref { margin: 0 0 8px; font-size: var(--text-2xs); color: #94a3b8; }
.finding-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.finding-entry {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 10px;
  align-items: center;
  width: 100%;
  padding: 8px 10px;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  background: #fff;
  cursor: pointer;
  text-align: left;
}
.finding-entry:hover { border-color: #94a3b8; background: #f8fafc; }
.finding-entry.is-selected { border-color: #2563eb; background: #eff6ff; }
.finding-entry-main { display: grid; min-width: 0; }
.finding-entry-main code { font-size: var(--text-xs); color: #0f172a; }
.finding-entry-main small {
  color: #64748b;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.finding-msg { font-size: var(--text-2xs); color: #64748b; }

.detail-viewer { position: sticky; top: 16px; min-width: 0; }
/* Title and the Context button share the header row; the title is a check id
   plus a field path and must truncate rather than push the button out. */
.viewer-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.viewer-head h2 { margin: 0; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.viewer-head .button { flex: 0 0 auto; }
.code-view {
  margin: 10px 0 0;
  max-height: 62vh;
  overflow: auto;
  background: #0f172a;
  border-radius: 10px;
  padding: 10px 0;
  font-size: var(--text-xs);
  line-height: 1.55;
}
.code-line { display: grid; grid-template-columns: 44px 1fr; position: relative; }
.code-line .ln {
  text-align: right;
  padding-right: 10px;
  color: #64748b;
  user-select: none;
}
.code-line .lc { color: #e2e8f0; white-space: pre; padding-right: 12px; }
.code-line.is-highlighted { background: rgba(250, 204, 21, 0.16); }
.code-line.is-highlighted .lc { color: #fde68a; }
.code-line.is-highlighted .ln { color: #fbbf24; }
.code-line.is-highlighted::before {
  content: "";
  position: absolute;
  left: 0;
  width: 3px;
  height: 1.55em;
  background: #fbbf24;
}
@media (max-width: 1100px) {
  .detail-layout { grid-template-columns: 1fr; }
  .detail-viewer { position: static; }
}

/* ---------- Visualization page ---------- */
/* Tabs are controls in a row: the floor between two of them is the one
   from the scale, not a number that happened to look tight enough. */
.viz-tabs { display: flex; gap: var(--space-2); margin-bottom: 14px; }
.viz-tab {
  padding: 8px 16px;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  background: #fff;
  cursor: pointer;
  color: #475569;
  font-weight: 600;
}
.viz-tab.is-active { background: #2563eb; color: #fff; border-color: #2563eb; }
.viz-tab:disabled { opacity: 0.4; cursor: default; }
.check-tabs { margin: 14px 0 10px; }

/* Timing tolerance, at the head of the factsheet tab: a settings row rather
   than a check row, so it reads as something that governs the checks below. */
.tolerance-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 18px;
  margin-bottom: 12px;
  padding: 14px 16px;
}
.tolerance-bar > div:first-child { flex: 1 1 320px; }
.tolerance-bar strong { display: block; font-size: var(--text-md); }
.tolerance-bar span { color: var(--muted); font-size: var(--text-sm); }
.tolerance-bar label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.unit-field { display: flex; align-items: center; gap: 6px; }
.unit-field input { width: 5.5rem; }
.unit-field > span { font-size: var(--text-sm); font-weight: 600; color: var(--muted); }
.viz-toolbar {
  display: flex;
  gap: 16px;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
/* The legend under the layout drawing. Deleted on 16.08.2026 with the page
   these rules were first written for, while lif_detail.html went on using
   them: every swatch is an empty span, so with no size and no colour the
   legend collapsed into one run of words - "node station other layout has
   action has finding". Found by looking at a screenshot of the page. */
.viz-legend {
  display: inline-flex;
  gap: 10px;
  align-items: center;
  font-size: var(--text-xs);
  color: #64748b;
}
.viz-legend .lg {
  width: 14px;
  height: 14px;
  border-radius: 4px;
  display: inline-block;
  vertical-align: middle;
}
.lg-base { background: #2563eb; }
.lg-action { background: #f59e0b; }
.viz-compat {
  margin: 0 0 12px;
  padding: 8px 12px;
  border-radius: 8px;
  font-size: var(--text-sm);
  line-height: 1.5;
  border: 1px solid #334155;
  background: #0b1220;
  color: #cbd5e1;
}
.viz-compat .cm { color: #94a3b8; font-weight: 600; }
.viz-compat.is-ok { border-color: #15803d; background: #0b1f14; color: #bbf7d0; }
.viz-compat.is-warn { border-color: #b45309; background: #241606; color: #fde68a; }
.viz-compat.is-fail { border-color: #b91c1c; background: #250d0d; color: #fecaca; }
.viz-compat.is-unknown { border-color: #334155; background: #0b1220; color: #cbd5e1; }

.vz-lif-edge { stroke: #334155; stroke-width: 1.5; }
.vz-lif-node { fill: #1e293b; stroke: #475569; stroke-width: 1; }

/* The order's facts, under the picker: one line, the full width of the box.
   Wrapping is what it did in the timeline's head, and three lines of small
   grey text above a picture is a caption nobody reads. A recording with a very
   long orderId scrolls sideways rather than breaking the line. */
.viz-order-facts {
  margin: 10px 0 0;
  padding-top: 10px;
  border-top: 1px solid var(--line);
  white-space: nowrap;
  overflow-x: auto;
}
.viz-order-facts:empty { display: none; }

.viz-canvas-wrap { position: relative; }
#order-svg {
  width: 100%;
  height: auto;
  background: #0f172a;
  border-radius: 12px;
  border: 1px solid #1e293b;
}
.vz-edge { stroke: #3b82f6; stroke-width: 3; }
.vz-edge.vz-horizon { stroke: #64748b; stroke-dasharray: 6 5; }
.vz-edge.vz-has-action { stroke: #f59e0b; }
.vz-node { fill: #3b82f6; stroke: #0f172a; stroke-width: 2; cursor: pointer; }
.vz-node.vz-horizon { fill: #64748b; }
/* Behind the vehicle. The same fill the order timeline gives its passed lanes,
   so the two pictures of one order agree on what a shade means. Declared after
   the horizon rules and repeated for both shapes, because an element that is
   released and behind the vehicle matches both selectors. */
.vz-edge.vz-passed { stroke: #25446e; }
.vz-node.vz-passed { fill: #25446e; }
.vz-action-ring { fill: none; stroke: #f59e0b; stroke-width: 2.5; }
.vz-label { fill: #e2e8f0; font-size: var(--text-xs); font-weight: 600; }
.vz-node-g { cursor: pointer; }
/* The vehicle sits above the path it is driving, and takes no clicks:
   the node under it stays reachable for its tooltip. */
.vz-agv { pointer-events: none; }
.viz-tooltip {
  position: absolute;
  z-index: 5;
  background: #0b1220;
  color: #e2e8f0;
  border: 1px solid #334155;
  border-radius: 8px;
  padding: 6px 10px;
  font-size: var(--text-xs);
  /* The box now carries the element's own JSON, so it has to be reachable:
     scrolled, selected, copied. It was pointer-events: none while it held two
     lines of summary, and that made it a picture of text rather than text. */
  pointer-events: auto;
  max-width: 420px;
}

/* Layout and timeline stand side by side: the same order as a shape and as a
   sequence. min-width:0 lets a grid column actually shrink, without which the
   timeline's own width would push the row wider than the page. */
.viz-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-5);
  align-items: start;
}
.viz-half { min-width: 0; }
@media (max-width: 1180px) {
  .viz-row { grid-template-columns: 1fr; }
}
.viz-picker { padding-bottom: 12px; }
.viz-picker .viz-toolbar { margin-bottom: 0; }
.viz-half .card-header { margin-bottom: 12px; }
/* The LIF verdict now sits under the layout it judges, not above it. */
.viz-half .viz-compat { margin: 12px 0 0; }
.viz-half h2 { font-size: var(--text-lg); margin: 0; }
/* The timeline header carries a control as well as the detach button: the two
   readings of an updated order are a choice the reader makes while looking. */
.viz-head-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-shrink: 0;
}
.viz-view-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-sm);
  color: var(--muted);
}
.viz-view-toggle select { margin: 0; }

/* The step control floats over the page and is dragged by its grip, so it can
   be parked wherever it does not cover the picture being read. */
.viz-float {
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 30;
  background: rgba(255, 255, 255, 0.97);
  border: 1px solid rgba(216, 226, 239, 0.96);
  border-radius: var(--radius-lg);
  box-shadow: 0 10px 30px rgba(16, 24, 40, 0.18);
  padding: 8px 10px 10px;
  min-width: 210px;
}
/* The handle a movable panel is dragged by, wherever one appears. */
.panel-grip {
  cursor: grab;
  user-select: none;
  touch-action: none;
  font-size: var(--text-xs);
  color: var(--muted);
  padding-bottom: 6px;
  white-space: nowrap;
}
.is-dragging .panel-grip { cursor: grabbing; }
.is-dragging { box-shadow: 0 16px 40px rgba(16, 24, 40, 0.26); }
.viz-float-buttons { display: flex; gap: 6px; }
.viz-float-buttons .button { flex: 1; justify-content: center; }
/* The order jumps sit on the outside and stay narrow: they are the coarse
   step, and the message step is the one the hand reaches for. */
.viz-float-buttons .button.is-jump { flex: 0 0 auto; padding-left: 9px; padding-right: 9px; }
.viz-float-scrub {
  width: 100%;
  margin: 10px 0 0;
  accent-color: var(--accent-2);
  cursor: grab;
}
.viz-float-scrub:active { cursor: grabbing; }

/* Order timeline — shares the dark canvas of the layout beside it, so the two
   read as one instrument rather than two unrelated pictures. */
#timeline-svg {
  width: 100%;
  height: auto;
  display: block;
  background: #0f172a;
  border-radius: 12px;
  border: 1px solid #1e293b;
  cursor: crosshair;
}

/* The two messages the picture is made of: what the master last sent, and
   what the vehicle last answered. Side by side on a wide screen, stacked on a
   narrow one — reading them next to each other is the whole point. */
.msg-pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
  gap: var(--space-5);
}
.msg-box h2 {
  font-size: var(--text-md);
  margin: 0 0 10px;
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
}
.msg-when {
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--muted);
}
.msg-rows {
  display: grid;
  grid-template-columns: minmax(9rem, auto) 1fr;
  gap: 2px 14px;
  margin: 0;
  font-size: var(--text-sm);
}
.msg-rows dt { color: var(--muted); }
.msg-rows dd { margin: 0; font-variant-numeric: tabular-nums; }
.msg-sub {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  margin: 14px 0 6px;
}
/* Long orders and long action lists scroll inside their box instead of
   pushing the timeline off the screen. */
.msg-scroll { max-height: 260px; overflow: auto; margin-top: 12px; }
.msg-box h2 .button { margin-left: auto; }

/* The message itself. Native dialog, our box — wide, because a state message
   is deep and wrapping it would cost the shape that makes it readable. */
.raw-dialog {
  border: 0;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  padding: 20px;
  width: min(860px, 94vw);
  max-height: 86vh;
}
.raw-dialog::backdrop { background: rgba(15, 23, 42, 0.45); }
.raw-dialog h2 { margin: 0; font-size: var(--text-lg); }
.raw-json-body {
  margin: 0;
  padding: 14px 16px;
  background: #0f172a;
  color: #e2e8f0;
  border-radius: var(--radius-md);
  font-family: var(--mono);
  font-size: var(--text-xs);
  line-height: 1.5;
  max-height: 66vh;
  overflow: auto;
  white-space: pre;
}
.msg-table { width: 100%; border-collapse: collapse; font-size: var(--text-xs); }
.msg-table th {
  text-align: left;
  font-weight: 600;
  color: var(--muted);
  padding: 4px 8px 4px 0;
  position: sticky;
  top: 0;
  background: #fff;      /* the panel's own ground, so rows scroll under it */
}
.msg-table td { padding: 3px 8px 3px 0; border-top: 1px solid #eef2f7; vertical-align: top; }
.msg-table code { font-size: inherit; }
/* A small state label — a badge that is read, not pressed: a partner's badge,
   whether a requirement file is still offered, how a request was answered.
   The partner pages used `.pill` from the day they were written and nothing
   defined it, so every one of them rendered as plain text in a column headed
   "State". Same shape as the tags below it, which is what the house already
   uses for exactly this. */
.pill {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 999px;
  background: #eef2f7;
  color: var(--muted);
  font-size: var(--text-2xs);
  font-weight: 600;
  white-space: nowrap;
}
.pill-ok { background: #dcfce7; color: #166534; }
.pill-bad { background: #fee2e2; color: var(--danger); }
/* Neither good nor bad: a verdict that stands, with something worth reading. */
.pill-warn { background: #fef3c7; color: #92400e; }
.pill.muted { background: #eef2f7; color: var(--muted); }
/* A pill that is also the way out of what it names. It has to keep the shape
   of the chip beside it - a reader tells a condition from a control by what
   it says, not by whether it happens to be a button - so the button reset
   that every other button carries is undone here rather than fought with a
   pile of !important further down. */
button.pill-button {
  border: 0;
  cursor: pointer;
  font-family: inherit;
  line-height: 1.2;
}
button.pill-button:hover { filter: brightness(0.95); }

/* A label that belongs to the box under it rather than to the space above.
   The editor's headings floated between two things and read as if they
   belonged to whichever the eye reached first. */
.sits-on-the-box { margin-bottom: 4px; }
.sits-on-the-box + * { margin-top: 0; }

/* A number and what it means, on one line: the label carries the unit and the
   answer to "what does zero do", so putting the field under it left the
   sentence and its answer a line apart. */
.field-inline {
  display: flex;
  align-items: center;
  gap: 10px;
}
.field-inline > span { flex: 1 1 auto; }
.field-inline > input { flex: 0 0 96px; }

.tag-base, .tag-horizon, .tag-status {
  display: inline-block;
  padding: 0 6px;
  border-radius: 999px;
  font-size: var(--text-2xs);
  font-weight: 600;
  white-space: nowrap;
}
.tag-base { background: #dbeafe; color: #1e40af; }
.tag-horizon { background: #eef2f7; color: #64748b; }
.tag-status { background: #f1f5f9; color: #334155; }

.diff-output { overflow-x: auto; }
.diff-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.diff-table th, .diff-table td {
  text-align: left;
  padding: 6px 10px;
  border-bottom: 1px solid #e2e8f0;
  vertical-align: top;
}
.diff-table code { font-size: var(--text-xs); color: #0f172a; }
.diff-changed { background: #fef9c3; }
.diff-added { background: #dcfce7; }
.diff-removed { background: #fee2e2; }

/* Honeypot: kept in the layout flow but off-screen, so people never see it
   while naive form-filling bots still find and complete it. Not display:none,
   which some bots specifically skip. Screen readers are excluded via
   aria-hidden and the field is removed from the tab order. */
.hp-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Preview marking for pre-release specifications (currently M2X 0.9.0) */
.preview-note {
  border-left: 4px solid var(--warning);
  margin-bottom: 16px;
}
.preview-badge {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: 999px;
  background: #fef3c7;
  color: #854d0e;
  font-size: var(--text-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* LIF layout graph controls */
.viz-controls { display: inline-flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.viz-controls .button.compact { min-width: 34px; text-align: center; }
.toggle-inline {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--text-xs);
  font-weight: 600;
  color: #475569;
  cursor: pointer;
}
.toggle-inline input { width: auto; margin: 0; }

/* LIF graph tooltip error line */
.viz-tooltip .tip-error { color: #fca5a5; font-weight: 600; }
/* The element exactly as the file wrote it, under everything the summary
   above already said. Bounded and scrollable: a node with a dozen vehicle
   types is longer than any screen, and a box that grew to fit it would cover
   the layout it is about. */
.viz-tooltip .tip-json {
  margin: 6px 0 0;
  padding: 6px 8px;
  max-height: 260px;
  overflow: auto;
  background: #060b14;
  border: 1px solid #1e293b;
  border-radius: 6px;
  font-family: var(--mono);
  font-size: var(--text-2xs);
  line-height: 1.45;
  white-space: pre;
  color: #cbd5e1;
}
.viz-tooltip .tip-note { margin: 6px 0 0; color: #94a3b8; }

/* Run export dialog */
.export-card { width: min(520px, 100%); max-height: none; }
.export-body { padding: 18px 20px 20px; display: grid; gap: 14px; }
.export-body .form-section { display: grid; gap: 8px; margin: 0; }
/* Compact on purpose: with the full field padding this input stood 47px
   tall inside a 23px line, and the option it belongs to towered over the
   one above it. */
.inline-number { width: 92px; display: inline-block; margin: 0 4px; padding: 5px 8px; }

/* Dashboard: combined metric tiles and nested run rows */
.metric-of { font-size: 0.5em; font-weight: 700; color: var(--muted); }
.nested-runs { padding: 4px 8px; }
.nested-runs .runs-table { box-shadow: none; border: 0; background: transparent; margin: 0; }
.nested-runs .runs-table td { border-bottom: 1px solid #f1f5f9; padding: 4px 8px; }
.nested-runs .runs-table tr:last-child td { border-bottom: 0; }
.profile-summary-row > td { vertical-align: middle; }

/* Dashboard: runs listed under their profile */
.profile-first-row > td { border-top: 2px solid var(--line); }
.profile-first-row:first-child > td { border-top: 0; }

/* Run inspector: clearly separated from the profile list above it */
.run-inspector {
  margin-top: 26px;
  border: 2px solid var(--accent-2);
  border-radius: var(--radius-lg);
  background: rgba(255, 255, 255, 0.6);
  box-shadow: 0 18px 40px rgba(37, 99, 235, 0.12);
  overflow: hidden;
}
.ri-banner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  padding: 16px 20px;
  background: linear-gradient(120deg, #1d4ed8, #0f766e);
  color: #fff;
}
.ri-banner h2 { margin: 2px 0 4px; color: #fff; }
.ri-banner p { margin: 0; color: rgba(255, 255, 255, 0.85); font-size: var(--text-sm); }
.ri-eyebrow {
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.75);
}
.ri-banner .button { background: rgba(255, 255, 255, 0.16); box-shadow: none; }
.run-inspector > .metrics,
.run-inspector > .inspector,
.run-inspector > .hint { margin: 16px; }

/* Findings grouped by the message they were found in.
   The list is a compact index by default; a row opens on click. */
.fg {
  border-bottom: 1px solid #eef2f7;
  background: #fff;
}
.fg:last-child { border-bottom: 0; }
.fg-head {
  display: grid;
  grid-template-columns: 14px 9px 1fr auto auto;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 7px 10px;
  background: transparent;
  border: 0;
  border-left: 3px solid transparent;
  border-radius: 0;
  box-shadow: none;
  cursor: pointer;
  font: inherit;
  color: inherit;
  text-align: left;
}
.fg-head:hover { background: #f6f9fd; transform: none; }
.fg.sev-critical .fg-head { border-left-color: #b91c1c; }
.fg.sev-major .fg-head { border-left-color: #dc2626; }
.fg.sev-minor .fg-head { border-left-color: #ca8a04; }
.fg.sev-info .fg-head { border-left-color: #2563eb; }
.fg.is-selected .fg-head { background: #eff6ff; }
.fg-caret { color: var(--muted); font-size: var(--text-2xs); text-align: center; }
.fg-dot { width: 9px; height: 9px; border-radius: 50%; background: #94a3b8; }
.fg.sev-critical .fg-dot, .fg.sev-major .fg-dot { background: #dc2626; }
.fg.sev-minor .fg-dot { background: #ca8a04; }
.fg.sev-info .fg-dot { background: #2563eb; }
.fg-main { min-width: 0; display: block; }
.fg-topic {
  display: block;
  font-weight: 600;
  font-size: var(--text-xs);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.fg-meta {
  display: block;
  font-size: var(--text-2xs);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.fg-count { font-size: var(--text-2xs); color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.fg-head .severity-count { font-size: var(--text-2xs); padding: 2px 7px; }
/* Closed by default - only an opened row shows its findings. */
.fg-body { display: none; }
.fg.is-open .fg-body {
  display: grid;
  gap: 6px;
  padding: 2px 12px 10px 36px;
  background: #fbfdff;
}
.fg-finding { border-left: 3px solid #cbd5e1; padding: 3px 0 3px 9px; }
.fg-finding.sev-critical { border-left-color: #b91c1c; }
.fg-finding.sev-major { border-left-color: #dc2626; }
.fg-finding.sev-minor { border-left-color: #ca8a04; }
.fg-finding.sev-info { border-left-color: #2563eb; }
.ff-head { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.ff-head .severity-count { font-size: var(--text-2xs); padding: 2px 7px; }
.ff-id { font-size: var(--text-xs); font-weight: 700; }
.ff-desc { display: block; margin-top: 2px; font-size: var(--text-xs); color: #475569; }
.fi-path { font-size: var(--text-2xs); background: rgba(15, 23, 42, 0.06); border-radius: 4px; padding: 1px 6px; }

/* Which checks a run applied, and why the others did not. */
.coverage-table td { vertical-align: middle; }
.coverage-group td {
  background: #f1f5f9;
  font-weight: 700;
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 6px 10px;
}
.coverage-row.cov-disabled td, .coverage-row.cov-unknown td { color: #94a3b8; }
.coverage-row.cov-disabled code, .coverage-row.cov-unknown code { opacity: 0.65; }
.cov-chip {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: var(--text-2xs);
  font-weight: 700;
  white-space: nowrap;
}
.cov-chip-evaluated { background: #dcfce7; color: #166534; }
.cov-chip-no_data { background: #e0f2fe; color: #075985; }
.cov-chip-disabled { background: #e2e8f0; color: #475569; }
.cov-chip-unknown { background: #f1f5f9; color: #64748b; }
.cov-chip-pass { background: #dcfce7; color: #166534; }
.cov-chip-fail { background: #fee2e2; color: #991b1b; }

/* Vehicle filter: a second row of chips under the severity filter. */
.vehicle-bar { margin-top: -4px; }
.filter-label {
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  align-self: center;
  margin-right: 2px;
}
.fg-vehicle { font-weight: 600; }

/* Enterprise management: short forms sit on one line, actions stay compact. */
.form-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 12px;
}
.form-row label { flex: 1 1 220px; margin: 0; }
/* Two rows of fields stacked had nothing between them: `margin: 0` above kills
   the label's own spacing, and the row's `gap` only separates the fields
   WITHIN a row. So the second row's caption sat directly against the box above
   it and the form read as one dense block. The gap here is the row gap, so
   vertical and horizontal spacing match. */
.form-row + .form-row { margin-top: 16px; }
/* The language picker in the top bar: as tall as the pills beside it, and
   narrow, because it holds three words and not a sentence. */
.lang-form select {
  padding: 4px 8px;
  font-size: var(--text-sm);
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #fff;
  color: var(--ink);
}
/* The same picker on the pages a visitor sees before signing in. Out of the
   flow, because those pages run a full-width band it must not interrupt, and
   over it, because the band is what it sits on. */
.lang-form--loose {
  position: absolute; top: 14px; right: 20px; z-index: 5;
}
.lang-form--loose select { background: rgba(255, 255, 255, 0.92); }
/* Present for a screen reader, absent for everyone else - the picker's meaning
   is obvious by sight and would only be noise beside the account name. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* And the block of rows keeps its distance from whatever follows - the twin
   table in the fleet dialog stood on top of the last field. */
.admin-form > .form-row:last-child { margin-bottom: 4px; }
.form-row .toggle-row { flex: 0 0 auto; align-self: center; }
.form-row button { flex: 0 0 auto; }
/* One cell per setting, each a field with its own button underneath. The
   forms used to be flex rows aligned to their own bottom edge, so a card with
   five of them put five buttons at five different heights.

   Two columns, fixed: auto-fit gave four at desktop width and then dropped the
   last setting onto a row of its own with three empty cells beside it.

   No rule under it any more. The grid used to close one long card of every
   setting an enterprise has, where a line was the only thing separating
   "rename" from "suspend everybody"; now each group of settings is a panel of
   its own and the panels do the separating. */
.setting-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px 24px;
}
@media (max-width: 900px) { .setting-grid { grid-template-columns: 1fr; } }
/* The button sits at the bottom of its cell, so the buttons of one row line up
   even when one cell holds two fields and the next holds one. */
.setting { display: flex; flex-direction: column; }
.setting > label { margin: 0; }
.setting .form-grid { gap: 12px 16px; }
.setting .actions { margin: 12px 0 0; }
.setting > .actions:last-child { margin-top: auto; padding-top: 12px; }
/* Adding a member is the one frequent action, so it sits on one line directly
   above the list it changes rather than in the grid of settings. */
.setting-add {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 12px;
  margin-bottom: 16px;
}
.setting-add > label:first-child { flex: 1 1 240px; margin: 0; }
.setting-add .toggle-row { align-self: end; }
.row-actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
/* The actions beside a card's heading. It had no rule at all, so two controls
   in one sat against each other on every page that put two there. */
.table-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
/* Inside a table the cell has to stay a cell. `display: flex` on a <td>
   overrides `table-cell`, the table wraps it in an anonymous cell of its own,
   and the row's boxes fall out of line - every separator in the last column
   was drawn half a row below the ones beside it. The buttons keep their
   spacing as inline boxes instead. */
td.row-actions { display: table-cell; white-space: nowrap; }
td.row-actions > * { display: inline-flex; vertical-align: middle; margin-left: 0; }
/* A cell full of controls. The floor of the scale, not a number chosen
   here: 6px used to be, and it read as two buttons stuck together. */
td.row-actions > * + * { margin-left: var(--space-2); }
.inline-field { display: inline-flex; align-items: center; gap: 6px; }
.inline-field input { width: 8em; }
/* The reporting block only shows the fields of the chosen mode. */
#report-fields { display: grid; gap: 12px; margin-top: 10px; }

/* `hidden` must win over layout rules: .form-grid { display: grid } and friends
   are more specific than the UA's [hidden] { display: none }, so a block marked
   hidden stayed visible. */
[hidden] { display: none !important; }

/* Admin: occasional forms fold away, the user list groups by enterprise. */
.admin-form { margin-bottom: 12px; }
.admin-form > summary {
  cursor: pointer;
  font-weight: 700;
  font-size: var(--text-md);
  list-style: none;
}
.admin-form > summary::before { content: "▸ "; color: var(--muted); }
.admin-form[open] > summary::before { content: "▾ "; }
.admin-form > summary + * { margin-top: 14px; }
.search-box { margin: 0; min-width: 260px; }
.search-box input { width: 100%; }
.user-group + .user-group { margin-top: 6px; }
.user-group-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 14px 0 6px;
  padding: 0 4px;
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  cursor: pointer;
}
.user-group-head::marker { color: var(--muted); }
/* Every column the same share of the line, wrapping instead of pushing: the
   admin reads the list as a grid, and one long email must not reflow it. */
.user-table { table-layout: fixed; width: 100%; }
.user-table th, .user-table td { overflow-wrap: break-word; }
.user-group-count {
  background: #e2e8f0;
  color: #475569;
  border-radius: 999px;
  padding: 1px 8px;
  font-size: var(--text-2xs);
  letter-spacing: 0;
}
.row-manage > summary { cursor: pointer; font-size: var(--text-xs); font-weight: 600; white-space: nowrap; }
.row-manage-body { display: grid; gap: 6px; padding-top: 6px; min-width: 220px; }
.row-manage-body form { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.row-manage-body input { max-width: 150px; }

/* Topbar identity: who you are, which enterprise you act in, and what that
   lets you do. */
.user-context {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 6px 12px;
  border-radius: 999px;
  background: rgba(15, 23, 42, 0.06);
  color: #334155;
  font-weight: 700;
  font-size: var(--text-sm);
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.role-pill {
  display: inline-flex;
  align-items: center;
  padding: 4px 10px;
  border-radius: 999px;
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.role-admin { background: #dcfce7; color: #166534; }
.role-superuser { background: #dbeafe; color: #1e40af; }
.role-member { background: #e2e8f0; color: #475569; }
@media (max-width: 720px) {
  /* On a narrow bar the name and the role are what matter. */
  .user-context { display: none; }
}

/* Dashboard: the sections are separate topics, not one long list. */
.dash-section + .dash-section {
  margin-top: 34px;
  padding-top: 26px;
  border-top: 1px solid var(--line);
}
.refresh-note {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border-radius: 999px;
  background: rgba(15, 23, 42, 0.05);
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.refresh-note::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #22c55e;
}

/* Project filter: a selector that sits in a page's action row. */
.project-filter { display: inline-flex; align-items: center; margin: 0; }
.project-filter label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.project-filter select { min-width: 190px; margin: 0; }

/* Action columns: icons instead of a row of labelled buttons, which wrapped
   into two lines once the owner and project columns arrived. */
.icon-actions {
  white-space: nowrap;
  /* A row, not a line of text. Between inline-flex children the whitespace in
     the markup renders as a space of its own and the real distance is the
     margin plus whatever the font makes of it; a flex container drops the
     whitespace and the floor below is the whole answer. */
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.icon-actions > *,
.icon-actions .inline-form {
  display: inline-flex;
  vertical-align: middle;
  margin: 0;
}

.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  /* No margin of its own: every row that holds icon buttons spaces them
     (.icon-actions, .replay-controls, .run-row-actions). A margin here was a
     second answer to the same question, and the one that won - it sits later
     in the file than the rules that zero it. */
  margin: 0;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #fff;
  color: #334155;
  cursor: pointer;
  transition: background .12s, color .12s, border-color .12s;
}
.icon-button svg { width: 17px; height: 17px; fill: currentColor; }
.icon-button:hover { background: #eff6ff; border-color: #bfdbfe; color: var(--navy-deep); }
.icon-button.primary { border-color: #bfdbfe; color: var(--navy-deep); background: #eff6ff; }
.icon-button.primary:hover { background: #dbeafe; }
/* button.danger paints a solid red button; an icon needs the colour on the
   glyph, not behind it, so the background is reset here. */
button.icon-button.danger,
.icon-button.danger { background: #fff; border-color: var(--line); color: #b91c1c; }
button.icon-button.danger:hover,
.icon-button.danger:hover { background: #fee2e2; border-color: #fecaca; color: #991b1b; }
.icon-button:focus-visible { outline: 2px solid var(--navy-deep); outline-offset: 2px; }

/* A completed project is kept for reference, so its row reads as inactive. */
tr.is-completed td { color: var(--muted); }
tr.is-completed strong { font-weight: 600; }

/* No project yet: say so instead of leaving a gap where the selector belongs. */
.project-filter-empty {
  display: inline-flex;
  align-items: center;
  padding: 6px 12px;
  border: 1px dashed var(--line);
  border-radius: 999px;
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.project-filter-empty:hover { border-color: #bfdbfe; color: var(--navy-deep); }

/* The project reads as context, the profile name as the subject. */
.profile-project { color: var(--muted); font-weight: 700; }

/* The interactive mission-plan editor: the map on the left, missions and
   actions on the right, stacking on a narrow viewport. */
.map-editor { display: grid; grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr); gap: 16px; align-items: start; }
@media (max-width: 900px) { .map-editor { grid-template-columns: 1fr; } }
.map-editor-canvas svg { cursor: crosshair; }
.map-editor-toolbar { display: flex; gap: 8px; align-items: center; margin-top: 10px; flex-wrap: wrap; }
.map-editor-side { display: flex; flex-direction: column; gap: 14px; }
.map-editor-missions { display: flex; flex-direction: column; gap: 10px; }
.map-editor-mission { border: 1px solid var(--line); border-radius: 10px; padding: 10px; }
.map-editor-mission.active { border-color: var(--accent); background: rgba(15, 118, 110, 0.06); }
/* Ticking a scenario into a plan, and saying where in the plan it goes. Sits
   with the row's other controls because it is a property of that row, not of
   the form at the foot of the list the boxes actually belong to. */
/* Remote control: the broker bar. The dropdown, Connect and Manage Brokers
   belong on one line - choosing a connection and opening it is one gesture,
   and stacked they read as three unrelated controls. */
.remote-bar { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; }
.remote-bar label { flex: 1 1 320px; min-width: 0; margin: 0; }
.remote-bar select { width: 100%; }
.remote-bar > .button, .remote-bar > button { margin-bottom: 2px; }

/* The events over a hall map. Dark on a field that runs green to red, with a
   pale ring so a dot stays findable on the red as well as on the green - the
   places with the most dots are exactly the places the field is darkest. */
.hall-events circle { fill: #10151c; fill-opacity: 0.62; stroke: #ffffff;
  stroke-opacity: 0.75; stroke-width: 0.08; pointer-events: all; }
.hall-events circle:hover { fill-opacity: 0.9; }

.map-editor-mission-head { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
/* The name keeps a readable width and the buttons drop to the next line rather
   than squeezing it to three letters, which is what a narrow side panel did. */
.map-editor-mission-head input[type="text"] { flex: 1 1 150px; min-width: 0; }
/* What is ticked onto the route, under the route. The picker closes and takes
   its ticks with it; the map only says an element carries something, not which
   of the three it offers. */
.map-editor-mission-actions { list-style: none; margin: 8px 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 4px; }
.map-editor-mission-actions li { display: flex; gap: 6px; align-items: center;
  font-size: var(--text-xs); }
.map-editor-action-where { color: var(--muted); font-family: var(--mono);
  flex: 0 0 auto; }
.map-editor-mission-actions li > span:nth-child(2) { flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; }

.map-editor-mission-row { display: flex; gap: 6px; align-items: center; margin-top: 8px; flex-wrap: wrap; }
.map-editor-mission-row select { flex: 1; min-width: 0; }
.map-editor-mission-summary { color: var(--muted); font-size: var(--text-xs); margin-top: 6px; }
#editor-action-picker-list { list-style: none; margin: 8px 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
#editor-action-picker-list li { display: flex; gap: 8px; align-items: flex-start; }
#editor-action-picker-list li.illegal { opacity: 0.55; }
#editor-action-picker-list label { display: flex; gap: 6px; align-items: flex-start; }
.map-editor-picker-heading { font-weight: 700; font-size: var(--text-xs); color: var(--muted); text-transform: uppercase; letter-spacing: 0.03em; margin-top: 4px; }
.map-editor-picker-heading:first-child { margin-top: 0; }

/* The control desk's two lists, one line per entry. An order is its number,
   a route summary and five small controls; an instant action is its number and
   four fields. The frame each card had at full size made six of them a page. */
.map-editor-mission.compact { padding: 6px 8px; border-radius: 8px; }
.map-editor-mission.compact .map-editor-mission-head { gap: 6px; }
.map-editor-mission.compact .map-editor-mission-head select { flex: 1 1 110px; min-width: 0; }
.map-editor-mission.compact .map-editor-mission-head input[type="number"] { width: 64px; }
.map-editor-mission.compact .map-editor-mission-summary { margin-top: 3px; }
.map-editor-mission.compact .map-editor-mission-row { margin-top: 4px; }
.map-editor-mission.compact .button.compact { padding: 2px 7px; font-size: var(--text-xs); }
.map-editor-order-number { font-weight: 700; font-size: var(--text-xs); color: var(--muted);
  font-family: var(--mono); flex: 0 0 auto; min-width: 1.6em; }
.map-editor-list-head { display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 4px; }
.map-editor-list-head h2 { margin: 0; }

/* The vehicles standing on the broker, as tiles to choose from. One tile is
   one vehicle: its name, whether it is talking, where it says it stands. The
   chosen one is outlined, and the map above draws the same choice. */
.vehicle-tiles { display: grid; gap: 8px;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }
.vehicle-tile { display: flex; flex-direction: column; gap: 3px; text-align: left;
  padding: 8px 10px; border: 1px solid var(--line); border-radius: 10px;
  background: #fff; cursor: pointer; font: inherit; color: inherit; }
.vehicle-tile:hover { border-color: var(--accent); }
.vehicle-tile.selected { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(15, 118, 110, 0.25);
  background: rgba(15, 118, 110, 0.06); }
.vehicle-tile.silent { opacity: 0.7; }
.vehicle-tile .name { font-weight: 700; font-family: var(--mono); font-size: var(--text-sm);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vehicle-tile .facts { display: flex; flex-wrap: wrap; gap: 4px 8px; font-size: var(--text-xs);
  color: var(--muted); }
.vehicle-tile .pill { font-size: var(--text-xs); }
.vehicle-bar { display: flex; align-items: center; justify-content: space-between; gap: 10px;
  flex-wrap: wrap; margin-bottom: 8px; }
.vehicle-bar h2 { margin: 0; }

/* Live view: a map that fills its panel, and two panels of detail below it. */
.live-head-meta { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 0; }
.live-map { background: #0f172a; border-radius: 12px; overflow: hidden; }
.live-map svg { display: block; width: 100%; height: auto; }
.live-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  align-items: center;
  margin: 10px 0 4px;
  font-size: var(--text-xs);
  color: var(--muted);
}
.live-legend i { display: inline-block; width: 12px; height: 12px; border-radius: 3px; margin-right: 5px; vertical-align: -1px; }
/* Fixed layout so a column's width no longer follows its content: live values
   change every frame, and an auto table re-flowed the columns on each update,
   making them jump. Longer values are clipped — acceptable here. */
.veh-table { table-layout: fixed; width: 100%; }
.veh-table td, .veh-table th {
  padding: 6px 10px; font-size: var(--text-xs);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 6px; }
/* The diff reads left to right: what it was, what it became. */
.diff-table { width: 100%; border-collapse: collapse; }
.diff-table td {
  padding: 5px 8px;
  font-size: var(--text-xs);
  border-bottom: 1px solid #eef2f7;
  font-family: var(--mono);
}
.diff-table td.k { color: #475569; font-family: inherit; }
.diff-row.changed td.a { background: #fee2e2; color: #991b1b; text-decoration: line-through; }
.diff-row.changed td.b { background: #dcfce7; color: #166534; font-weight: 700; }
.diff-row.same td { color: #94a3b8; }

/* Replay: transport controls above a scrub bar, both full width. */
/* The transport hovers as a floating overlay pinned to the bottom of the
   viewport, so it stays reachable whether you are looking at the map or the
   state diff — no need for a second copy further down the page. */
.replay-bar > .panel-grip {
  text-align: center;
  padding: 0 0 2px;
  line-height: 1;
}
.replay-bar {
  display: grid;
  gap: 6px;
  position: fixed;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  z-index: 40;
  width: max-content;
  max-width: calc(100% - 24px);
  padding: 8px 12px;
  box-shadow: 0 10px 30px rgba(15, 23, 42, 0.22);
  backdrop-filter: blur(4px);
  background: color-mix(in srgb, var(--bg) 90%, transparent);
}
/* The floating bar sits over the foot of the page; keep the last content clear
   of it. */
.replay-scroll-guard { height: 76px; }
.replay-controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.replay-controls .icon-button { font-size: var(--text-xs); line-height: 1; }
/* The second transport beside the State diff heading: compact, and it pushes
   the vehicle picker to the right edge of the header. */
.replay-speed {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 0 auto;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.replay-speed select { margin: 0; min-width: 110px; }
#rp-scrub { width: min(340px, 60vw); margin: 0; accent-color: var(--navy-deep); }
.replay-position {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-variant-numeric: tabular-nums;
  font-size: var(--text-sm);
  font-weight: 700;
}
.replay-position .muted { font-weight: 600; }

/* ------------------------- selecting several rows ------------------------- */
/* The tick column stays narrow and out of the way; the bar that acts on the
   selection only appears once something is ticked, so a list nobody is tidying
   looks exactly as it did before. */
.bulk-col {
  width: 34px;
  padding-right: 0 !important;
  text-align: center;
}
.bulk-col input[type="checkbox"] {
  width: 15px;
  height: 15px;
  margin: 0;
  vertical-align: middle;
  accent-color: var(--navy-deep);
  cursor: pointer;
}
.bulk-form { display: none; }
.table-header-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}
.bulk-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 6px 5px 12px;
  border: 1px solid #fecaca;
  border-radius: 999px;
  background: #fef2f2;
}
.bulk-bar[hidden] { display: none; }
.bulk-count {
  font-size: var(--text-xs);
  font-weight: 700;
  color: #991b1b;
  white-space: nowrap;
}
.runs-toolbar {
  display: flex;
  justify-content: flex-end;
  margin-top: 10px;
}
.runs-toolbar .bulk-bar { margin: 0; }

/* --------------------------- the chosen upload ---------------------------- */
/* Once a file is picked the box stops inviting and starts confirming: the
   document itself, by name and size, so nobody submits the wrong log. */
.file-picker .has-file {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  padding: 14px 20px;
  border: 1px solid #bfdbfe;
  border-radius: var(--radius-md);
  background: #fff;
  text-align: left;
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.06);
}
.file-picker .fp-icon {
  width: 34px;
  height: 34px;
  flex: 0 0 auto;
  color: var(--navy-deep);
}
.file-picker .fp-text { display: block; }
.file-picker .fp-text strong {
  font-size: var(--text-md);
  word-break: break-all;
}
.file-picker .fp-text small { color: var(--muted); }

/* Editing a project happens in a dialog; the row only holds the trigger. */
.modal-card.edit-card { width: min(560px, 100%); }
.modal-body {
  display: grid;
  gap: 14px;
  padding: 18px 20px 20px;
}
.modal-body .actions { display: flex; gap: 10px; }

/* An irreversible action gets its own corner of the page, not a button that
   sits next to "Change password" as if it were of the same weight. */
.danger-zone {
  border-color: #fecaca;
  background: #fff7f7;
}
.danger-zone h2 { color: #991b1b; }
.danger-zone summary {
  cursor: pointer;
  font-weight: 700;
  color: #991b1b;
}
.danger-zone[open] summary { margin-bottom: 10px; }
.danger-zone details > form { display: grid; gap: 12px; margin-top: 12px; }

/* ------------------------- the administrator's overview ------------------- */
/* Everything in the installation on one page: grouped by enterprise, then by
   the user who produced it, with one row per report, layout or log. */
.filter-panel label.grow { flex: 1; }
.filter-panel .hint { margin-bottom: 8px; white-space: nowrap; }
.work-user {
  border-top: 1px solid var(--line);
  padding: 14px 20px 6px;
}
.work-user[hidden] { display: none; }
.work-user-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 8px;
}
.work-user-head strong { font-size: var(--text-md); }
.work-user-head .hint { margin: 0 0 0 auto; }
.work-user .table-scroll table { font-size: var(--text-sm); }
.kind-chip {
  display: inline-block;
  min-width: 62px;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: 0.03em;
  text-align: center;
  text-transform: uppercase;
}
.kind-report { background: #eff6ff; color: #1d4ed8; }
.kind-lif { background: #f0fdf4; color: #15803d; }
.kind-log { background: #fefce8; color: #a16207; }


/* ---------------------------------------------------------------- wall ----
   One box per capture profile, as on the MQTT page, with the vehicles it has
   heard as a list underneath. Colour says one thing per row: is this vehicle
   moving, standing, quiet, or in trouble. */
.wall-strip {
  /* One row, always: column auto-flow gives every tile an equal share of the
     line, however many the strip carries - seven while a capture runs, and
     the inventory strip's handful further down. */
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 10px;
  margin-bottom: 16px;
}

.wall-stat {
  background: #fff;
  border: 1px solid #e2e8f0;
  border-radius: 10px;
  padding: 10px 14px;
}

.wall-stat strong { display: block; font-size: var(--text-2xl); line-height: 1.1; color: #12356b; }
.wall-stat span { font-size: var(--text-2xs); text-transform: uppercase; letter-spacing: 0.08em; color: #64748b; }
.wall-stat.sev-critical strong { color: #dc2626; }
.wall-stat.sev-major strong { color: #ea580c; }
.wall-stat.sev-minor strong { color: #ca8a04; }
.wall-stat.sev-warning strong { color: #b45309; }
.wall-stat.sev-info strong { color: #2563eb; }

/* The run counts of a profile, in the pills the run list uses, between what
   the profile is and what can be done with it. */
.profile-severity { display: flex; gap: 6px; align-items: center; margin-left: auto; padding-right: 14px; }
.profile-severity[hidden] { display: none; }

.wall-pulse { font-size: var(--text-xs); color: #64748b; align-self: center; }
/* The simulator's controls sit at the top, so they need air under them. */
.sim-actions { margin-bottom: 18px; }

/* What the workspace holds, under what it is doing. */
/* auto-fit rather than a fixed four: an account whose plan carries no custom
   rulesets gets three tiles, and a fixed grid would leave a hole where the
   fourth used to be. The min() keeps a single column on a narrow screen
   without a breakpoint of its own. */
.inventory-strip { grid-template-columns: repeat(auto-fit, minmax(min(100%, 170px), 1fr)); margin-top: 22px; }
.inventory-strip .wall-stat { text-decoration: none; display: block; }
.inventory-strip .wall-stat:hover { border-color: #94a3b8; }
.inventory-lists { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 1fr)); gap: 14px; }
.recent-list { list-style: none; margin: 0; padding: 0; }
.recent-list li { padding: 7px 0; border-top: 1px solid #eef2f7; font-size: var(--text-sm); }
.recent-list li:first-child { border-top: none; }
.recent-list a { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.recent-list .muted { font-size: var(--text-xs); }

/* The last few replay sources, offered as plain links inside their forms. */
.card-note {
  display: block;
  margin-top: 2px;
  font-size: var(--text-xs);
  font-weight: 500;
  color: #64748b;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.recent-replays form { display: flex; align-items: baseline; gap: 8px; }
.linklike {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--navy);
  cursor: pointer;
  text-align: left;
}
.linklike:hover { text-decoration: underline; }
.linklike:disabled { color: #94a3b8; cursor: default; text-decoration: none; }
.live-chip.muted { color: #94a3b8; }
/* Recording, said in the colour the rest of the application uses for a thing
   that is running. A chip that only ever reads "not recording" in grey leaves
   the reader checking the message count to find out. */
.live-chip.live {
  color: #0f766e;
  background: #ccfbf1;
  border-color: #99f6e4;
  font-weight: 700;
}

/* The vehicle list inside a profile box. */
.veh-state {
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: #64748b;
}

tr.veh-driving .veh-state { color: #16a34a; }
tr.veh-silent .veh-state { color: #94a3b8; }
tr.veh-warning .veh-state { color: #c2410c; }
tr.veh-error .veh-state { color: #b91c1c; }
tr.veh-warning td { background: #fff7ed; }
tr.veh-error td { background: #fef2f2; }
tr.veh-silent td { color: #94a3b8; }

.v-uninit { color: #b45309; font-weight: 700; }


/* --- message context inside the live view --------------------------------
   The window that opens when a finding is clicked: what was found, and the
   message it was found in with the offending field marked. */
.list-more { display: flex; align-items: center; gap: 10px; margin-top: 8px; }
.ctx-finding {
  margin: 0 0 6px;
  padding-left: 8px;
  border-left: 3px solid #cbd5e1;
  font-size: var(--text-sm);
}

.ctx-finding.sev-critical { border-left-color: #dc2626; }
.ctx-finding.sev-major { border-left-color: #ea580c; }
.ctx-finding.sev-minor { border-left-color: #ca8a04; }
.ctx-finding.sev-info { border-left-color: #2563eb; }
.ctx-path { display: block; font-family: var(--mono); font-size: var(--text-xs); color: #64748b; }
#ctx-payload { max-height: 44vh; overflow: auto; }
.fg-head[role="button"] { cursor: pointer; }
.fg-head[role="button"]:hover { background: #f1f5f9; }

/* The finding rows in the live list carry their own text, not just a colour. */
.fg-items { list-style: none; margin: 4px 0 0; padding: 0 0 0 26px; }
.fg-items li {
  font-size: var(--text-sm);
  line-height: 1.45;
  color: #334155;
  padding: 2px 0 2px 8px;
  border-left: 3px solid #cbd5e1;
}

.fg-items li.sev-critical { border-left-color: #dc2626; }
.fg-items li.sev-major { border-left-color: #ea580c; }
.fg-items li.sev-minor { border-left-color: #ca8a04; }
.fg-items li.sev-info { border-left-color: #2563eb; }
.fg-items code { font-size: var(--text-xs); color: #12356b; }
.fg-path { font-family: var(--mono); font-size: var(--text-2xs); color: #64748b; }
.fg-rest { color: #94a3b8; border-left-color: transparent !important; }

/* What a report was made from, above the verdict it reached. */
.run-scope .scope-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px 18px; margin: 0; }
.run-scope dt { font-size: var(--text-2xs); text-transform: uppercase; letter-spacing: 0.07em; color: #94a3b8; }
.run-scope dd { margin: 2px 0 0; font-size: var(--text-md); overflow: hidden; text-overflow: ellipsis; }
.run-scope h3 { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.06em; color: #64748b; margin: 14px 0 6px; }
/* A caveat next to the field it qualifies. Quiet on purpose: it must be
   findable without competing with the value it stands beside. */
.run-scope dd .scope-note { display: block; margin-top: 3px; font-size: var(--text-2xs); color: #64748b; }
.run-scope dd .scope-note:hover { color: #334155; }
/* A passage quoted from the standard, set apart so our words and the
   committee's cannot be confused for one another. */
.spec-quote { margin: 0 0 14px; padding: 10px 14px; border-left: 3px solid #94a3b8; background: #f8fafc; }
.spec-quote p { margin: 0 0 6px; }
.spec-quote p:last-child { margin-bottom: 0; }
.type-counts { list-style: none; display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; font-size: var(--text-sm); }
.type-counts li { background: #f1f5f9; border-radius: 999px; padding: 3px 11px; }
.type-counts .tc-count { font-weight: 700; color: #12356b; }
.run-scope h3 .scope-count { background: #12356b; color: #fff; border-radius: 999px; padding: 1px 9px; margin-left: 6px; font-size: var(--text-xs); }
.type-counts.vehicle-names li { font-family: var(--mono); }
/* What the vehicle was measured against, inside its own pill: the factsheet's
   file name, or "from the traffic". The pill grows a second line for it. */
.vehicle-names li .veh-sheet {
  display: block;
  font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
  font-size: var(--text-2xs);
  color: #64748b;
  margin-top: 1px;
}
/* Measured or not, said in the list itself: an unmeasured vehicle standing
   next to a measured one must not read as a passing one. */
.type-counts.vehicle-names li.veh-measured { background: #dcfce7; color: #14532d; }
.type-counts.vehicle-names li.veh-measured::before { content: "✓ "; font-weight: 700; }
.type-counts.vehicle-names li.veh-unmeasured { background: #fef3c7; color: #78350f; }
.type-counts.vehicle-names li.veh-unmeasured::before { content: "○ "; font-weight: 700; }
.run-scope .error-hint { color: #92400e; margin: 8px 0 0; }
/* A switch that changes what gets measured belongs next to the thing it
   changes, reading as one sentence rather than as a second field. */
.inline-check { display: flex; align-items: center; gap: 8px; margin: 8px 0 0;
  font-weight: 500; font-size: var(--text-sm); text-transform: none; letter-spacing: 0; }
.inline-check input { width: auto; margin: 0; }

/* Which body of rules raised a finding. Five families, five muted grounds -
   quiet enough not to fight the severity pill beside them, distinct enough to
   be told apart at a glance down the column. The custom amber is the same one
   the complete verdict uses, so a reader meets it twice and learns it once. */
.fam {
  display: inline-block;
  padding: 2px 7px;
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.fam-vda { background: #e8eefb; color: #12356b; border-color: #c9d8f4; }
.fam-fs  { background: #e4f2ef; color: #0f5f58; border-color: #bfe0d9; }
.fam-m2x { background: #efeafc; color: #5b21b6; border-color: #ddd2f6; }
.fam-lif { background: #eef2f7; color: #44546b; border-color: #dae1ea; }
.fam-cr  { background: #fdf3e3; color: #78350f; border-color: #f0dcb7; }
/* A classification, not prose: the column takes what the badge needs and no
   more, so the sentence columns keep the room. */
.fam-cell { white-space: nowrap; width: 1%; }

/* ---- custom rulesets ---------------------------------------------------- */
/* The complete verdict stands next to the VDA5050 one, related but visibly
   its own row: an amber edge, because it includes rules the standard does
   not know. */
.complete-metrics { border-left: 3px solid #b7791f; }
.complete-metrics > div > strong { color: #78350f; }

/* Its own full-width row in the report: four columns, one of them a sentence.
   The id and the counts never wrap, so the label gets whatever is left. */
.custom-checks th:first-child, .custom-checks td:first-child { white-space: nowrap; }
.custom-checks th:nth-child(3), .custom-checks td:nth-child(3) { white-space: nowrap; }
.custom-checks th:last-child, .custom-checks td:last-child { text-align: right; width: 1%; white-space: nowrap; }
.custom-checks tr.muted td { color: #94a3b8; }

/* Nothing of its own any more: a custom ruleset's checks are built from the
   same .check-grid / .check-group / .check-row markup as every other tab, so
   they inherit its styling. Only the spacing where the profile form stacks
   the lists under the picker is left to say. */
.ruleset-checks { margin-top: 12px; }
/* A custom ruleset's tab sits in the same strip as the VDA5050 ones but carries the
   amber edge the complete verdict uses, so a glance says whose rules these
   are. Inside a tab the fieldset is already the whole panel, so it drops the
   extra top margin. */
.viz-tab.custom-tab { border-left: 3px solid #b7791f; }
.viz-tab.custom-tab.is-active { border-left-color: #78350f; }
.check-tab-panel .ruleset-checks { margin-top: 0; }
.custom-note { margin: 0 0 12px; color: var(--muted); font-size: var(--text-sm); max-width: 80ch; }

.ruleset-check { padding: 12px 0; border-top: 1px solid #eef2f7; }
.ruleset-check:first-child { border-top: none; }
.rc-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.rc-head .rc-id { font-size: var(--text-xs); color: #12356b; background: #f1f5f9; border-radius: 4px; padding: 1px 7px; }
.ruleset-check p { margin: 6px 0 0; max-width: 75ch; }
.rc-rule { margin-top: 6px; }
.rc-rule pre { background: #0f172a; color: #e2e8f0; border-radius: 8px; padding: 10px 12px; font-size: var(--text-xs); overflow-x: auto; }
.rc-rule summary { cursor: pointer; color: #64748b; font-size: var(--text-sm); }
tr.muted td { color: #94a3b8; }

/* A per-version run in the report list: the summary line reads like a normal
   run (white), its version reports nested underneath on a light grey and a
   touch smaller. */
tr.group-row td { background: transparent; }
.group-toggle { display: inline-flex; align-items: center; gap: 8px; background: none; border: 0; padding: 0; font: inherit; color: inherit; cursor: pointer; }
.group-toggle .group-caret { transition: transform .12s ease; color: var(--muted); }
.group-toggle.is-open .group-caret { transform: rotate(90deg); }
tr.subrow td { background: #f4f6f8; font-size: var(--text-2xs); }
tr.subrow .severity-count, tr.subrow .score-badge, tr.subrow .badge { font-size: var(--text-2xs); }
/* The version behind the run id, on every row now rather than only on the
   members of a per-version run. Quieter than the id it follows: the id is what
   you look for, the version is what tells you which of two runs of one file
   you are looking at. */
.row-lead { color: var(--muted); font-weight: 600; font-size: var(--text-2xs); margin-left: .35rem; }

/* The waiver line under a disposition is a secondary offer, not a heading. */
.toggle-row .toggle-note { font-size: var(--text-sm); font-weight: 400; color: var(--muted); }

/* The package download belongs to the verdict it exports, so it sits in that
   box, on the line of its heading, pushed to the right edge. */
.verdict-card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.verdict-card-head h2 { margin: 0; }

/* Zoom controls read as magnifiers, so the glyph says what it does without a
   tooltip. Square, so + and - keep the same footprint. */
.button.icon-only { display: inline-flex; align-items: center; justify-content: center; padding: 6px; }
.mag-icon { width: 1.15em; height: 1.15em; display: block; }

/* The platform mark beside the workspace dot: present, not loud. */
.platform-mark { height: 32px; width: auto; display: block; flex: 0 0 auto; }
.logo-preview { max-height: 64px; width: auto; display: block; margin: 8px 0 12px; }

/* ============================== Report tabs =============================== */
/* Findings and Check Coverage share one box: two views of the same run, so a
   tab rather than a second card. */
.report-tabs { display: flex; gap: 6px; margin-bottom: 6px; }
.report-tab {
  border: 1px solid #d7e0ea;
  border-radius: 8px 8px 0 0;
  background: #f1f5f9;
  padding: 8px 18px;
  font-size: var(--text-md);
  font-weight: 700;
  color: #475569;
  cursor: pointer;
}
.report-tab.is-active {
  background: #fff;
  color: #12356b;
  border-bottom-color: #fff;
}
.report-tab:hover:not(.is-active) { background: #e2e8f0; }

/* The coverage summary next to the verdict: counts only, the full list lives
   in the Check Coverage tab below. */
.coverage-summary { width: 100%; border-collapse: collapse; }
.coverage-summary th, .coverage-summary td {
  padding: 5px 8px;
  border-bottom: 1px solid #eef2f7;
  font-size: var(--text-sm);
  text-align: left;
}
.coverage-summary th.num, .coverage-summary td.num { text-align: right; }
.coverage-summary thead th { color: #64748b; font-size: var(--text-xs); }
.coverage-summary .cov-version { font-weight: 700; color: #12356b; vertical-align: top; }

/* The verdict table: findings pills sit before the score, so the pills say
   what the number cost. */
.verdict-scores .score-sev { text-align: right; white-space: nowrap; }
.verdict-scores .score-sev .severity-count { margin-left: 4px; }
.verdict-scores td.score-val { text-align: right; font-weight: 700; color: #12356b; width: 3.5em; }

/* ====================== Detailed findings as a tree ======================= */
/* An opened family must read as one block: the head keeps the accent, the
   children hang from a rail and step in, slightly smaller than the head. */
.detail-check.is-open { border-color: #2563eb; box-shadow: 0 1px 4px rgba(37, 99, 235, 0.12); }
.detail-check.is-open > .detail-check-head { background: #eff6ff; }
.detail-check.is-open > .detail-check-head .detail-check-title code { color: #12356b; }
.detail-check-body {
  margin-left: 14px;
  border-left: 3px solid #bfdbfe;
  padding-left: 14px;
}
.detail-check-body .finding-entry { font-size: var(--text-sm); padding: 6px 10px; }
.detail-check-body .finding-entry-main code { font-size: var(--text-2xs); }

/* =========================== Spacing, standing rule ====================== */
/* Two stacked things keep at least one step of air between them - boxes on a
   page, and everything inside a box just the same. It used to hold for the
   boxes only, which is how a paragraph came to sit flush under the grid it
   belongs to and a pill to touch the heading beside it.
   Adjacent margins collapse, so anything that brings more space keeps it;
   this is a floor, not a setting. Grids and flex rows bring their own gap and
   it must not fall under --space-3 either.
   Kept at the end of the file on purpose: it ties with the single-class rules
   of the components and wins by order, so a box cannot quietly opt out of the
   floor by setting margin: 0 somewhere above. Where a component really must
   sit tighter, it says so with two classes - and then it is a decision
   somebody wrote down rather than a default nobody chose. */
main.container > * + * { margin-top: var(--space-3); }
.panel > * + *,
.card > * + *,
.table-card > * + *,
.quick-card > * + *,
.modal-card > * + *,
.section-head > * + * { margin-top: var(--space-3); }
/* A row is not a stack, and this is the one part of the floor that reaches into
   a box's grandchildren. It is meant for the heading of a table header and the
   paragraph under it; put a row of controls in that slot instead - the twins
   page puts its two "new twin" buttons there - and every button but the first
   was pushed 12px down. In a centred flex row that splits into six pixels of
   crookedness on each of them, which is what it looked like. The rows named
   here bring their own gap, so nothing touches. */
.table-header > div:not(.table-actions, .page-actions, .row-actions, .icon-actions,
                        .actions, .action-row, .btn-row, .check-tools) > * + * {
  margin-top: var(--space-3);
}

/* A pill is a word in a box, and a box needs air on every side. In a row of
   them the row sets the gap; standing alone next to text it brings its own,
   so a severity chip cannot end up against the sentence it qualifies. */
:where(.severity-count, .score-pill, .coverage-pill, .verdict-pill, .run-badge,
       .perf-unacked, .tls-badge) { margin-right: var(--space-2); }
:where(.severity-count, .score-pill, .coverage-pill, .verdict-pill, .run-badge,
       .perf-unacked, .tls-badge):last-child { margin-right: 0; }
/* An inline code pill is a box in the middle of a sentence and needs the same
   air as any other box - without it the border sits against the word beside
   it and the line reads as one long token. */
:where(p, li, td, dd, summary, .hint, .rc-head) > code { margin: 0 2px; }
/* Except where a flex row already spaces them - a margin on top of a gap is
   two answers to one question. */
:where(.verdict-top, .verdict-severities, .type-counts, .icon-actions, .page-actions,
       .pick-list, .coverage-gates) :where(.severity-count, .score-pill, .coverage-pill,
       .verdict-pill, .run-badge, .perf-unacked, .tls-badge) { margin-right: 0; }

/* ========================= Performance review ============================ */
.performance-table th.num, .performance-table td.num { text-align: right; }
.performance-table .perf-span { font-size: var(--text-xs); color: #64748b; white-space: nowrap; }
.perf-error-link { font-weight: 700; color: #991b1b; text-decoration: underline; }
.perf-definitions { margin: 8px 0 0; padding-left: 18px; display: grid; gap: 6px; }
.perf-definitions li { font-size: var(--text-sm); color: #334155; }
.perf-unacked {
  display: block;
  font-size: var(--text-2xs);
  font-weight: 700;
  color: #991b1b;
  white-space: nowrap;
}

/* The timeframe control on the Performance Review. One line: the recording's
   own span, the two moments, the button. It is a setting for the page, not a
   subject of its own, so it takes a strip rather than a panel with a heading -
   the answer underneath is what the reader came for. */
.timeframe-bar { padding: 10px 14px; }
.timeframe-bar form {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  font-size: var(--text-sm);
}
.timeframe-span { margin-right: auto; color: var(--muted); }
.timeframe-span strong { color: var(--ink); margin-right: .5rem; }
.timeframe-span em { font-style: normal; }
.timeframe-to { color: var(--muted); }
.timeframe-bar input { width: 12.5rem; font-size: var(--text-sm); padding: 5px 8px; }
.timeframe-bar .error { margin: 8px 0 0; }

/* The rules of the Performance Review sit behind a footnote: a reader who
   knows them should not scroll past them under every table. */
.perf-footnote { margin: 14px 0 0; font-size: var(--text-xs); color: var(--muted); }
.linklike {
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  color: var(--accent-2);
  text-decoration: underline;
  cursor: pointer;
}
.modal-card.perf-help-card { width: min(720px, 100%); }

/* The partner card. Same shape as the other verdicts, because it is one — and
   the rows are read as a checklist, so the answer column stays narrow and the
   unmet rows carry the weight rather than a colour nobody can name. */
.partner-card .req-unmet td { font-weight: 600; }
.partner-card .req-unmet .score-val { color: var(--danger); }
.partner-card .req-met .score-val { color: var(--green); }
.partner-card .req-not_applicable td { color: var(--muted); }
.req-file { border-top: 1px solid var(--line); padding-top: var(--space-3); margin-top: var(--space-3); }
.req-file:first-of-type { border-top: 0; padding-top: 0; margin-top: 0; }
.pick-partners .partner-files { margin-top: var(--space-2); }

/* An unanswered request, at the top of the dashboard. Tinted rather than
   alarming: somebody is waiting for an answer, which is worth seeing first and
   is not a fault. Drawn only when there is one, so the tint stays meaningful. */
.partner-waiting {
  border-left: 3px solid var(--accent);
  background: linear-gradient(180deg, #f7fbff, #ffffff);
}
.partner-waiting h2 { margin-bottom: var(--space-2); }
.partner-waiting table td { vertical-align: middle; }

/* The partner picker: the vendor on the left, their files on the right, so the
   two steps read as one row rather than as a form that grew. */
.partner-pick {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr);
  gap: var(--space-4);
  align-items: start;
  padding: var(--space-3) 0 0;
}
@media (max-width: 900px) { .partner-pick { grid-template-columns: 1fr; } }
.partner-files-column .choice { margin-top: var(--space-2); }
.req-dialog { width: min(760px, 94vw); }
.req-dialog .verdict-scores td:first-child { max-width: 34rem; }

/* The check-profile matrix: 201 rows, four version columns. A changed cell is
   marked, because the whole page exists to answer "what differs here". The
   room between the cells is deliberate - four selects shoulder to shoulder
   read as one control, and the text needs air from the cell borders. */
.profile-matrix { border-collapse: separate; border-spacing: 0; }
.profile-matrix th { padding: 14px 26px 14px 22px; text-align: left; }
.profile-matrix td { vertical-align: middle; padding: 16px 26px 16px 22px; }
.profile-matrix td:first-child, .profile-matrix th:first-child { padding-left: 26px; }
.profile-matrix td:first-child { max-width: 380px; padding-right: 34px; }
.profile-matrix td:first-child code { display: inline-block; margin-bottom: 4px; }
.profile-matrix tbody td { border-bottom: 1px solid #edf1f5; }
.profile-matrix select { width: auto; min-width: 122px; padding: 9px 14px; }
.profile-matrix select.level-changed { border-color: #d97706; background: #fffbeb; font-weight: 700; }
.profile-matrix small { line-height: 1.45; display: block; padding-right: 8px; }
/* A version without this rule: a dash, centred where the select would sit. */
.profile-matrix .level-void { display: inline-block; min-width: 122px; text-align: center; color: #94a3b8; }
/* The group fold around the matrix: the table must not touch the fold's
   frame, and the summary line needs room of its own. */
.check-group > summary { padding: 6px 4px; }
.check-group .table-scroll { margin-top: 12px; padding-bottom: 4px; }

/* The plan form: one fieldset per vehicle type, two compact rows inside -
   who drives, and what work they get. An unticked type fades and submits
   nothing; a ticked one is readable in one glance. */
.plan-form h2 { margin: 18px 0 6px; }
.plan-head-row { display: flex; gap: 18px; align-items: end; flex-wrap: wrap; }
.plan-head-row .plan-title { flex: 1 1 320px; }
.plan-type { border: 1px solid var(--line); border-radius: 10px; padding: 12px 16px; margin: 10px 0; }
.plan-type-row { display: flex; gap: 16px; align-items: end; flex-wrap: wrap; }
.plan-type-row label { margin: 0; }
.plan-type-row input[type="number"] { width: 96px; }
.plan-type-pick { flex: 0 0 auto; min-width: 180px; }
.plan-sheet { flex: 1 1 240px; }
.plan-work-row { margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--line); }
.plan-type-off { opacity: 0.45; }
.plan-type-off .plan-work-row { display: none; }

/* A list of partners to tick, on the fleet that may be started by them. Five
   names stand as a plain stack; past that the box scrolls, because a group with
   forty links must not push the save button off the page. The height is five
   rows and their gaps exactly, so the sixth row is half-visible and says so. */
.pick-scroll { display: flex; flex-direction: column; gap: var(--space-3); }
.pick-scroll.tall {
  max-height: 267px;
  overflow-y: auto;
  padding: var(--space-3);
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #fff;
}

/* A run button with the device it will drive against beside it. The two belong
   together - a button alone would leave the choice looking like a setting
   somewhere else on the page - so they sit in one row with the ordinary gap
   between controls, and wrap together when the head row runs out of width. */
.device-run { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin: 0; }
.device-run label.inline { margin: 0; font-size: var(--text-xs); }

/* The Pareto bar in Fleet Health: one fault type against the worst of them.
   A bar rather than a second number, because the point of a Pareto is the
   shape - two types and a tail - and a column of minutes hides it. */
.pareto-bar {
  display: inline-block; height: 10px; min-width: 2px; border-radius: 5px;
  background: linear-gradient(90deg, var(--accent, #2F80ED), #93CE44);
}

/* ---------- Fleet Health ------------------------------------------------- */

/* The fleet chart: one line per machine, sharing a pair of axes.
   Stretched to the page width with preserveAspectRatio="none", so every stroke
   needs non-scaling-stroke or the lines thin out as the box widens. */
.fleet-chart {
  display: block; width: 100%; height: 340px;
  background: linear-gradient(180deg, #fbfcfd, #f4f7fa);
  border: 1px solid var(--line); border-radius: 8px;
}
.fleet-chart .grid {
  stroke: var(--line); stroke-width: 1; vector-effect: non-scaling-stroke;
}
/* The time divisions run the other way and must not compete with the readings:
   dashed and paler than the value grid, so they read as the backdrop the lines
   cross rather than as lines of their own. */
.fleet-chart .grid-time {
  stroke: color-mix(in srgb, var(--line) 60%, transparent);
  stroke-dasharray: 3 4;
}
.fleet-chart .axis {
  font-size: var(--text-2xs); fill: var(--muted);
  /* The viewBox is stretched, so text would stretch with it. */
  vector-effect: non-scaling-stroke;
}
.fleet-chart polyline { fill: none; vector-effect: non-scaling-stroke; }
/* The visible line, and a fat invisible one under it: two pixels of stroke is
   not a target anybody can hit with a mouse, and the name lives on the hover. */
.fleet-chart .ink {
  stroke: var(--line); stroke-width: 1.6;
  stroke-linejoin: round; stroke-linecap: round;
}
.fleet-chart .hit { stroke: transparent; stroke-width: 11; }
/* Twenty lines at once is a thicket. Hovering one dims the rest, which is the
   whole reason a chart beats a grid here: the pack stays visible behind the
   machine being looked at. */
.fleet-chart:hover .trace { opacity: .18; }
.fleet-chart .trace:hover { opacity: 1; }
.fleet-chart .trace:hover .ink { stroke-width: 3; }

/* A single vehicle's trends. Each series keeps its own vertical scale - a
   voltage sag of half a volt and a distance of nine hundred metres share no
   axis, and one shared axis would flatten the sag to a straight line. */
.trend-grid {
  display: grid; gap: 20px 26px;
  grid-template-columns: repeat(auto-fit, minmax(310px, 1fr));
}
.trend { margin: 0; }
.trend figcaption {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 10px; margin-bottom: 4px;
}
.trend-label { font-size: var(--text-sm); color: var(--muted); }
.trend-latest { font-size: var(--text-md); font-weight: 600; color: var(--ink); }
/* How much of a service interval is gone, beside the figure it belongs to.
   Not green-until-100: a service planned for the week it falls due is a
   service planned on time, so the amber band opens a little before the limit
   and the red one a little after it. */
.pill.share-ok { background: rgba(38, 150, 84, 0.12); color: #1c6b3d; }
.pill.share-due { background: rgba(240, 170, 20, 0.20); color: #8a5d00; }
.pill.share-over { background: rgba(200, 25, 35, 0.12); color: var(--danger); }

/* A dialog for the thing that is set once. The service limits used to be a
   form of equal weight stacked above the one that records a service, so the
   rare act stood in front of the daily one every day. */
.sheet {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  max-width: 560px; width: calc(100% - 2rem);
  color: var(--ink);
}
.sheet::backdrop { background: rgba(10, 30, 60, 0.42); }
.sheet h2 { margin: 0 0 var(--space-2); }
.sheet-actions {
  display: flex; justify-content: flex-end; gap: var(--space-3);
  margin-top: var(--space-5);
}
/* Pushes one control to the far end of a row of fields. */
.push-right { margin-left: auto; }

/* One box per topic, and the pictures inside it. It used to be a card holding
   a header panel and a second panel of pictures - three frames around one
   idea, and two of them saying nothing. */
.trend-topic { margin-bottom: var(--space-5); }
.trend-topic h2 { margin: 0 0 var(--space-4); }
.spark {
  display: block; width: 100%; height: auto;
  background: linear-gradient(180deg, #fbfcfd, #f4f7fa);
  border: 1px solid var(--line); border-radius: 6px;
}
.spark polyline {
  fill: none; stroke: var(--accent); stroke-width: 2.5;
  stroke-linejoin: round; stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
/* The axes. The two numbers on the left are this series' own low and high -
   every series is scaled to itself, so those numbers are the only thing
   saying what the shape is worth. That used to be a paragraph under the page;
   it is now on the picture it describes. */
.spark .grid { stroke: var(--line); stroke-width: 1; vector-effect: non-scaling-stroke; }
.spark .grid-time {
  stroke: color-mix(in srgb, var(--line) 60%, transparent);
  stroke-dasharray: 3 4;
}
/* Size lives on the element: inside a viewBox a font-size is in user
   units, and this box renders at roughly 43% of them - a value from the
   page's type scale would come out at five pixels. */
.spark .axis { fill: var(--muted); }

/* The headline numbers above the trends. */
.figures {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
}
.figure { display: flex; flex-direction: column; gap: 2px; }
.figure-value { font-size: var(--text-2xl); font-weight: 600; color: var(--ink); }
.figure-label { font-size: var(--text-xs); color: var(--muted); }

.button.small { padding: 4px 10px; font-size: var(--text-xs); }

/* The per-fleet reading settings. */
.settings-grid {
  display: grid; gap: 16px 22px; margin-bottom: 18px;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.settings-grid label, .settings-danger label {
  display: flex; flex-direction: column; gap: 5px; font-weight: 500;
}
/* The confirmation is a checkbox and its sentence, side by side. Without this
   it inherits the column layout of the block it sits in and the box floats
   above its own words, centred, belonging to nothing. */
.settings-danger label.inline {
  flex-direction: row; align-items: center; justify-content: flex-start;
  gap: 8px; margin-top: 10px; font-weight: 600;
}
.settings-danger label.inline input { width: auto; margin: 0; }
.settings-grid .hint, .settings-danger .hint { font-weight: 400; margin: 0; }
/* Set apart because it is the only setting on the page that destroys
   anything, and it should not look like the others. */
.settings-danger {
  border-left: 3px solid #cf3b2f; padding: 14px 0 14px 16px; margin-bottom: 18px;
  background: linear-gradient(90deg, rgba(207,59,47,.05), transparent 60%);
}

/* The hall map: one rect per square metre, coloured in the browser so that
   taking a machine out of the picture costs nothing. The viewBox is metres,
   so the shape of the building decides the aspect and the page only decides
   how wide it is drawn. */
.hall-map {
  display: block; width: 100%; max-height: 520px;
  background: linear-gradient(180deg, #fbfcfd, #f4f7fa);
  border: 1px solid var(--line); border-radius: 8px;
}
.hall-map .squares rect { stroke: none; }
.hall-map .squares rect.empty { fill: none; }
/* The layout is a reference frame, drawn over the squares. Under them it is
   invisible - the aisles are exactly where the machines drive, so every line
   would sit beneath a coloured square. Over them it must not compete either,
   hence the thin dark line with a light halo: legible on green and on red. */
.hall-map .lif line {
  stroke: #1f2d3d; stroke-width: 1.1; stroke-opacity: .55;
  vector-effect: non-scaling-stroke;
}
.hall-map .lif circle {
  fill: #fff; stroke: #1f2d3d; stroke-width: 1; stroke-opacity: .7;
  vector-effect: non-scaling-stroke;
}
.hall-legend {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  font-size: var(--text-sm);
}
.hall-scale {
  display: inline-block; width: 160px; height: 12px; border-radius: 6px;
  background: linear-gradient(90deg, rgb(46,158,91), rgb(232,163,23), rgb(198,45,39));
}

/* The machine picker, under both pictures and driving both. */
.picker-head {
  display: flex; align-items: center; gap: 10px; margin-bottom: 10px;
}
.picker-boxes {
  display: grid; gap: 4px 18px;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
}
.pick {
  display: flex; align-items: center; gap: 7px;
  font-size: var(--text-sm); cursor: pointer;
}
.pick input { margin: 0; }
/* The swatch carries the line's own colour, so the box and the line in the
   chart above are recognisably the same machine without reading the name. */
.pick-swatch {
  width: 14px; height: 3px; border-radius: 2px; background: var(--line);
  flex: none;
}

/* The fleet wall: many fleets, small pictures, one question. */
.wall-grid {
  display: grid; gap: 18px;
  /* Wider than it was: a tile carries four pictures now, and two of them side
     by side need the room to stay readable. */
  grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
}
.wall-card { display: flex; flex-direction: column; gap: 10px; }
.wall-head { display: flex; flex-direction: column; gap: 2px; }
.wall-head h2 { margin: 0; }
.wall-head .muted { font-size: var(--text-xs); }
/* The figures on a wall tile: label left, value right, one line each. A table
   would be tidier and wrong - these are five separate facts about one fleet,
   not five rows of anything, and nothing in the next tile lines up with them. */
.wall-facts {
  display: grid; grid-template-columns: auto 1fr; gap: 3px 12px;
  margin: 0; font-size: var(--text-sm);
}
.wall-facts dt { color: var(--muted); }
.wall-facts dd { margin: 0; text-align: right; }
.wall-facts .pill { font-size: var(--text-xs); }
/* The flagged machines, each its own way in. They wrap and sit right like the
   rest of the column, and a linked one keeps the pill's colour rather than
   turning into blue underlined text in the middle of a row of pills. */
.wall-flagged {
  display: inline-flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end;
}
.wall-flagged a.pill { text-decoration: none; }
.wall-flagged a.pill:hover { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Declared against measured, on the vehicle page. A broken promise is the one
   figure on the page allowed to be red: it is the document and the machine
   disagreeing, which is the reading somebody opened the page for. */
.figure-value.bad { color: var(--danger); }
.promise-table td { vertical-align: top; }
.promise-table .muted { font-size: var(--text-xs); }
.promise-broken td:first-child { border-left: 3px solid var(--danger); }

/* Four pictures, two by two: the readings over time on top, the same fleet
   over its building underneath. Below the tile's own width they stack to one
   column rather than shrinking further - a hall map 70 px wide is a smudge. */
.wall-shots {
  display: grid; gap: 8px;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.wall-shot { display: block; text-decoration: none; min-width: 0; }
.wall-shot img {
  display: block; width: 100%; border: 1px solid var(--line); border-radius: 6px;
  background: #f7f9fb;
}
.wall-shot:hover img { border-color: var(--accent); }
.wall-caption {
  display: block; font-size: var(--text-xs); color: var(--muted); margin-top: 3px;
}

/* Fleet and Worth a look, side by side: one says what you are looking at, the
   other why you would. Half a row each, stacked once there is no room for
   two - a findings table squeezed into 300 px is unreadable either way. */
.fleet-row {
  display: grid; gap: 18px; align-items: start; margin-bottom: 18px;
  grid-template-columns: repeat(auto-fit, minmax(430px, 1fr));
}
.fleet-row > .panel { margin: 0; }
.fleet-row h2 { margin: 0 0 12px; }
/* Flat inside: no panel within a panel. A box in a box reads as two things
   when it is one. */
/* One line, because it is one question: which fleet, over what window, show
   it. Stacked, the fleet name sat on a row of its own above the two controls
   that qualify it, and the box was three rows tall for three fields. */
.fleet-pick form {
  display: flex;
  flex-direction: row;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 12px;
}
.fleet-pick label.wide { flex: 1 1 22ch; min-width: 22ch; }
.fleet-pick label.wide select { width: 100%; }
/* The wrapper stays for the pages that stack it; here its children join the
   one row rather than forming a second. */
.fleet-pick .pick-row { display: contents; }
.pick-row { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; }
.pick-row label { margin: 0; }
.worth-look .table-scroll { margin: 0; }
.worth-look table { font-size: var(--text-sm); }
.worth-look .hint { margin: 12px 0 0; }
.quiet-note { margin: 0; font-size: var(--text-sm); color: var(--muted); }

/* --------------------------------------------------------- benchmark rows --
   Two fleets side by side, one figure each. The only thing the eye has to do
   here is tell which side won and by how much, so the winner is the one that
   reads louder - bold, dark, with the margin beside it - and the other side
   stays plain. It was built the other way round once, with the better number
   shrunk into a grey chip next to a bold loser, and the table said the
   opposite of what it meant. */
.bench-figure { display: inline-flex; align-items: baseline; gap: 6px; }
.bench-value { font-variant-numeric: tabular-nums; font-size: var(--text-md); }
.bench-unit { color: var(--muted); font-size: var(--text-xs); }
.bench-none .bench-value { color: var(--muted); }

.bench-won .bench-value { font-weight: 700; color: var(--ink); }
.bench-figure:not(.bench-won) .bench-value { color: var(--muted); }

/* The margin is the reason the row is read at all: 8 % is a rounding error
   between two runs of the same fleet, 40 % is a decision. */
.bench-margin {
  padding: 1px 7px;
  border-radius: 999px;
  background: #dcfce7;
  color: #166534;
  font-size: var(--text-2xs);
  font-weight: 600;
  white-space: nowrap;
}

/* A heading that starts a new group of fields inside one form. The fields
   above it belong to the previous question, and without the gap the reader
   takes the first field under the heading for the last field over it. */
.form-break { margin-top: 28px; }

/* ------------------------------------------------------------- plan line --
   What you are on and what you bought on top of it, on one line. The features
   a plan carries and the ones it does not were two lists of twenty under a
   heading asking "what do we have"; they hang under the plan name now, where
   the person who wants them looks and nobody else scrolls past them. */
.plan-line { display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px 18px; }
.plan-line h2 { margin: 0; }
.plan-addons { margin-left: auto; }

.plan-name {
  position: relative;
  font-weight: 700;
  border-bottom: 1px dotted var(--muted);
  cursor: help;
}
/* The markup the layer shows for the plan name. Kept in the page so it is
   there without JavaScript - and out of the way, since it is a tooltip's
   worth of text in the middle of a heading. */
.plan-tip { display: none; }

/* ---------------------------------------------------------- uptime strip --
   A week of one connection, as a strip: green where it recorded, red where it
   dropped, and the bare track where it was not asked to run at all. That third
   state is why this is not a red-on-green bar - painting "switched off" red
   would report a connector as broken for every hour nobody wanted it. */
.uptime { margin: 14px 0 4px; }
.uptime-strip {
  position: relative;
  height: 26px;
  border-radius: 6px;
  background: repeating-linear-gradient(
    45deg, #eef2f7, #eef2f7 6px, #e6ebf2 6px, #e6ebf2 12px);
  overflow: hidden;
}
.uptime-span { position: absolute; top: 0; bottom: 0; }
.uptime-span.is-up { background: #2f9e5f; }
.uptime-span.is-down { background: var(--danger); }

.uptime-scale { position: relative; height: 18px; margin-top: 4px; }
.uptime-tick {
  position: absolute;
  transform: translateX(-50%);
  color: var(--muted);
  font-size: var(--text-2xs);
  white-space: nowrap;
}
/* The first and last marks would hang off the ends. */
.uptime-tick:first-child { transform: none; }
.uptime-tick:last-child { transform: translateX(-100%); }

.uptime-key { display: flex; align-items: center; gap: 6px; margin: 8px 0 0; }
.uptime-dot { display: inline-block; width: 10px; height: 10px; border-radius: 3px; }
.uptime-dot.is-up { background: #2f9e5f; }
.uptime-dot.is-down { background: var(--danger); }
.uptime-dot.is-off { background: #e6ebf2; }
.uptime-key .uptime-dot:not(:first-child) { margin-left: 12px; }

/* A report that has left the house. Quiet on the row - it is a fact about the
   report, not a warning - and the whole answer is on its tooltip: who, when,
   and whether they have opened it. */
.shared-mark {
  display: inline-block;
  padding: 1px 7px;
  border-radius: 999px;
  background: #e7f3ff;
  color: var(--accent-2);
  font-size: var(--text-2xs);
  font-weight: 600;
  white-space: nowrap;
  cursor: help;
}

/* The scenario's live window: the traffic as a console, always tailed. */
.remote-live-dialog { border: 0; border-radius: var(--radius-lg); box-shadow: var(--shadow-md);
  padding: 0; width: min(1100px, 94vw); }
.remote-live-dialog::backdrop { background: rgba(15, 23, 42, 0.45); }
.remote-live { padding: 20px; }
.remote-live-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.remote-live-console { margin: 12px 0; padding: 10px 12px; height: 60vh; overflow-y: auto;
  background: #0b1220; border-radius: 8px; font-family: var(--mono);
  font-size: var(--text-xs); line-height: 1.45; }
.remote-live-console .line { white-space: pre-wrap; word-break: break-all; color: #cbd5e1; }
.remote-live-console .line.outbound { color: #93c5fd; }
.remote-live-console .line.inbound { color: #86efac; }
.remote-live-console .line.notice { color: #64748b; font-style: italic; }

/* Watching something live: its own panels on the left, the hall on the right.
   They answer one question between them - a console cannot say whether the
   vehicle is moving, and a map cannot say what it was told - so they are read
   together or not at all. One column below the breakpoint, where side by side
   would leave two things too narrow to read instead of one. */
.watch-split { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
.watch-split.alone { grid-template-columns: 1fr; }
.watch-split-map .live-map { margin-top: 0; }
/* The console sets the height of the row, and the map follows it rather than
   the other way round: a map that grew with the hall would push the console
   off the screen on a wide layout. */
.watch-split-wire .remote-live-console { height: 46vh; margin-top: 0; }
.watch-split-map .table-scroll { margin-top: 10px; max-height: 22vh; overflow-y: auto; }
@media (max-width: 1100px) {
  .watch-split { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------------------
   Remote control, the twins and the fleets: the forms that moved into dialogs,
   the rows that grew a project picker, and the map's own hover card.
   --------------------------------------------------------------------------- */

/* A dialog is as tall as its content, and a fleet's line-up is a table of every
   twin an account may use. Without a ceiling the sheet grows past the viewport
   and its own buttons go with it. */
.sheet { max-height: 90vh; overflow: auto; }
/* The two forms that were pages before they were dialogs. A field row of three
   needs the room a 560px sheet does not have. */
.broker-sheet { max-width: 720px; }
.wide-sheet { max-width: 1000px; }
/* The part of a tall dialog that scrolls, so the heading and the buttons stay
   where they were put. */
.sheet-scroll { max-height: 46vh; overflow: auto; }
.sheet-scroll h3 { margin: var(--space-4) 0 var(--space-2); }
.sheet-scroll > h3:first-child { margin-top: 0; }

/* A field that carries a control of its own. The button sits inside the box the
   input draws, so a row of two fields keeps both the same height. */
.reveal-field { position: relative; display: block; }
.reveal-field input { padding-right: 40px; }
.reveal-toggle {
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  width: 28px;
  height: 28px;
  border-color: transparent;
  background: transparent;
  color: var(--muted);
}
.reveal-toggle:hover { background: #eff6ff; color: var(--navy-deep); }
.reveal-toggle.is-revealed { color: var(--navy-deep); }

/* A field with a button beside it rather than inside it - the button does
   something to the world (it listens on the broker), not to the field. */
.field-with-button { display: flex; gap: var(--space-2); align-items: center; }
.field-with-button input { flex: 1 1 auto; min-width: 0; }
.field-with-button button { flex: 0 0 auto; }

/* Widths inside a .form-row. Name and host earn twice a port's share of the
   line, and a port is five characters wide however much room is going. */
.form-row .grow-2 { flex: 2 1 220px; }
.form-row .grow-0 { flex: 0 0 auto; }
.form-row .field-port input { width: 7em; }

/* The one button above a list. Not in the page head: the thing it adds to is
   directly underneath it. */
.list-actions { margin: var(--space-4) 0; }

/* Remote control puts the order on the left and the layout on the right, in
   equal halves: the order is what is being written and the layout is what it is
   being written on, and neither is the other's sidebar. */
.remote-editor { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
@media (max-width: 900px) { .remote-editor { grid-template-columns: 1fr; } }
/* In a toolbar the tick is one control among buttons, not a field with a box
   around it. */
.map-editor-toolbar .toggle-row.compact {
  min-height: 0;
  padding: 4px 10px;
  align-self: center;
  font-size: var(--text-xs);
}
.map-editor-toolbar .toggle-row.compact input { width: auto; margin: 0; }

/* What an element is and what may be placed on it, while the pointer is still
   on it. Positioned against the map's own box. */
.live-map { position: relative; }
.map-editor-tip {
  position: absolute;
  z-index: 5;
  max-width: 280px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 10px;
  border-radius: 8px;
  background: rgba(15, 23, 42, 0.95);
  border: 1px solid #334155;
  color: #e2e8f0;
  font-size: var(--text-xs);
  line-height: 1.35;
  pointer-events: none;
  box-shadow: var(--shadow-md);
}
.map-editor-tip strong { color: #fff; font-family: var(--mono); }
/* An offer the layout makes but this vehicle may not take. Shown rather than
   hidden, and marked rather than shown as if it were on offer. */
.map-editor-tip .is-illegal { color: #fca5a5; }

/* What a fleet is made of, one twin per line inside its cell. */
.fleet-member { display: block; white-space: nowrap; }

/* The picker that files a row, standing with that row's actions rather than
   under its name. Two things keep it in line with the buttons beside it: its
   caption goes next to it rather than above it, because a two-line control in
   a line of one-line ones pulls the whole strip out of true, and it is sized
   to the compact buttons rather than to a form's fields. */
.profile-actions .row-project label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  white-space: nowrap;
}
.profile-actions .row-project select {
  min-width: 150px;
  font-size: var(--text-xs);
  padding: 6px 8px;
  margin: 0;
}

/* Inside a table the cell has to stay a cell. `display: flex` on a <td>
   overrides `table-cell`, the browser wraps it in an anonymous cell of its
   own, and the box falls out of the row: on the twins list the icon cell came
   out sixteen pixels shorter than the row it belongs to, and the row's rule
   was drawn across the middle of the last column. This is the same repair
   td.row-actions already carries; .icon-actions never got it. */
td.icon-actions { display: table-cell; white-space: nowrap; }
td.icon-actions > * { display: inline-flex; vertical-align: middle; margin: 0; }
td.icon-actions > * + * { margin-left: var(--space-2); }

/* The form a capture profile and a connector are made on. It was a fold at the
   top of its page; as a dialog it needs the width its check tree wants, which
   is more than a sheet of fields does. */
.profile-sheet { max-width: 1180px; }
/* Its two setup rows are two questions of two halves each, so exactly two
   columns rather than as many as auto-fill can fit: Project beside the layout,
   the rulesets beside the factsheets. */
@media (min-width: 60em) {
  .profile-setup { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}
/* The check tree inside the dialog folds away, the way the Analyze page holds
   it. Closed it is a line; open it is the tallest thing on the form. */
/* No display of its own: a summary that is turned into a flex box loses the
   browser's disclosure triangle, and a fold with no marker reads as a heading
   nobody can open. */
.profile-sheet .check-panel > summary { cursor: pointer; }
.profile-sheet .check-panel > summary strong { margin-right: var(--space-2); }
.profile-sheet .check-panel[open] > summary { margin-bottom: var(--space-3); }

/* The runs of one capture profile, behind a fold. Closed it is a line saying
   how many there are; a month of shifts is thirty rows standing between one
   profile and the next. */
.profile-runs > summary {
  cursor: pointer;
  font-weight: 700;
  color: var(--ink);
}
.profile-runs[open] > summary { margin-bottom: var(--space-3); }

/* The shell without its main menu. One page asks for it - the live view, which
   is only ever reached by a button that opens a window - and in that window a
   navigation would be an invitation to leave the one thing it was opened for.
   The column the sidebar held goes with it, and so does the button that would
   have slid it in on a narrow screen. */
/* `body .app-shell` further up is a guard against legacy styles and it fixes
   the two columns, so this one has to weigh the same and stand later - a bare
   class loses to it, and the page then rendered into the 281px column the
   sidebar was not in. */
body .app-shell--bare { grid-template-columns: minmax(0, 1fr); }
.app-shell--bare .menu-button { display: none; }

/* A certification run's matrix: what cannot be shown is still listed, greyed
   rather than hidden. A picker that silently omits the impossible teaches
   nobody why it is impossible. */
.cert-step.is-blocked td { color: var(--muted); }
.cert-step .cert-why { font-size: var(--text-xs); color: var(--muted); max-width: 46ch; }
/* The segments somebody has to walk to the vehicle for, marked down the side:
   a reader counting their own parts should not have to read every card. */
.cert-stop { border-left: 3px solid var(--warning); }


/* The fleet's vehicle row. One column for what the vehicle *is*, then the two
   switches and the plug - three columns for identity pushed the controls off
   the right of the page, and the topic is read once when a fleet controller is
   set up and never again. */
.veh-identity { line-height: 1.45; }
.veh-identity .veh-made-of,
.veh-identity .veh-topics {
  display: block;
  font-size: var(--text-xs);
  margin-top: var(--space-1);
}
.veh-identity .veh-topics code { font-size: inherit; }
/* Wide enough for the longest mode plus its warning, and no wider: the select
   used to take the width of the column and left the errors beside it cramped. */
select.veh-mode { max-width: 22ch; }
/* Chips over their buttons, and air between both - a standing fault and the
   way to clear it must not read as one control. */
.veh-errors { display: flex; flex-direction: column; gap: var(--space-2); }
.veh-error-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.veh-error-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
/* The state on a line of its own, the ways to change it under it: a chip
   wedged between two icons reads as a third button. */
.veh-bus { display: flex; flex-direction: column; gap: var(--space-2); align-items: flex-start; }
.veh-bus-actions { display: flex; gap: var(--space-2); align-items: center; }
/* These containers carry their own spacing, so the inline-form's own left
   margin would add a second, uneven one on top of the gap. */
.veh-error-chips .inline-form,
.veh-error-actions .inline-form,
.veh-bus-actions .inline-form { margin-left: 0; }

/* Project members: the picker on the create form and in the members dialog.
   One column of toggle rows that wraps into two on a wide screen. */
.member-picker { margin-top: 12px; }
.member-picker legend { font-weight: 600; }
.member-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 4px 16px; margin-top: 8px; }
.member-grid .toggle-row { margin: 0; }

/* All runs: one fold per enterprise, its heading the summary. */
.work-group > summary { cursor: pointer; list-style: none; }
.work-group > summary::-webkit-details-marker { display: none; }
.work-group > summary h2::before { content: "\25B8"; display: inline-block; width: 1.1em; color: var(--muted); }
.work-group[open] > summary h2::before { content: "\25BE"; }
.work-last { font-size: var(--text-sm); font-weight: 400; color: var(--muted); margin-left: 12px; }
/* The administrator's enterprise picker, and the pill that says the
   enterprise shown is borrowed. */
.view-as-form select { max-width: 220px; }
.user-context--borrowed { outline: 1px dashed var(--accent); outline-offset: 2px; }

/* The requirement editor: the eight sections left, the running summary right. */
.req-start { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.req-editor-toolbar { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; flex-wrap: wrap; }
.req-test { display: flex; align-items: flex-end; gap: 8px; }
.req-test select { min-width: 280px; }
.req-editor-grid { display: grid; grid-template-columns: minmax(0, 1fr) 260px; gap: 16px; align-items: start; }
.req-editor-aside { position: sticky; top: 12px; }
.req-section { margin-bottom: 12px; }
.req-section > summary .muted { font-weight: 400; }
.req-check-filter { display: flex; gap: 16px; align-items: center; margin: 8px 0 12px; }
.req-check-filter input[type="search"] { flex: 1; }
.req-check-group h3 { margin: 14px 0 6px; font-size: var(--text-md); }
.req-check-row { display: grid; grid-template-columns: minmax(0, 1fr) 190px; gap: 6px 12px; align-items: center; padding: 4px 0; border-bottom: 1px solid var(--line); }
.req-check-name { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; min-width: 0; }
.req-check-name code { font-size: var(--text-sm); }
.req-tolerance { grid-column: 1 / -1; display: grid; grid-template-columns: 110px minmax(0, 1fr); gap: 8px; padding-left: 12px; }
.req-ruleset-row { display: flex; gap: 18px; align-items: center; flex-wrap: wrap; padding: 6px 0; }
.req-summary dd { font-size: var(--text-sm); }
.req-row-met td:nth-child(2) { color: var(--accent); }
.req-row-unmet td:nth-child(2) { color: var(--danger); font-weight: 600; }
@media (max-width: 900px) { .req-editor-grid { grid-template-columns: 1fr; } .req-editor-aside { position: static; } }
.req-off-version { background: var(--bg); color: var(--muted); }
[data-off-version="1"] code { color: var(--muted); }
.api-token, .api-example { white-space: pre-wrap; word-break: break-all; font-size: var(--text-sm); padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); }

/* A progress bar, for the few answers that take long enough to need one.
   A class rather than markup of its own at the one place that has it: the
   next slow thing gets the same bar instead of a second one. The width of
   the fill is the one thing a script sets, because it is a measurement and
   not a palette decision - everything else is here. */
.progress { height: 6px; border-radius: 3px; background: var(--line); overflow: hidden; }
.progress > span { display: block; height: 100%; width: 0; background: var(--green); transition: width 180ms linear; }
.progress[hidden] { display: none; }
