/* Plain modern CSS: custom properties and layers, no build step (§17.4).
 *
 * Two ideas carry this interface, and everything else stays quiet:
 *
 *   1. The gate chain. This product is a sequence of gates — trusted domain,
 *      then enrollment, then storage — and the header renders that chain
 *      literally on every page, so the counts you see are the counts at each
 *      gate rather than a wall of unrelated tiles.
 *
 *   2. The status rail. Every row, panel and nav item carries a coloured left
 *      edge: the boundary a report either crosses or does not. One motif,
 *      repeated, doing the work that paragraphs of explanation used to.
 *
 * Colour is semantic, never decorative. Five signals, each meaning exactly one
 * state, so a screen can be read at a glance without labels.
 */

@layer tokens, base, shell, components;

@layer tokens {
  :root {
    color-scheme: dark light;

    --ground:     #0f131c;
    --surface:    #161c27;
    --raised:     #1d2531;
    --line:       #29323f;
    --line-soft:  #1f2733;
    /* Contrast is measured, not eyeballed. Against --surface these are
       14.2:1, 8.3:1 and 4.7:1 — the last one clears WCAG AA for normal text,
       which the previous value (3.3:1) did not. It is the colour the small
       uppercase labels use, so it was failing on the hardest text in the
       interface: 10px, letter-spaced, and dim. */
    --ink:        #e6eaf2;
    --ink-dim:    #aab5c9;
    --ink-faint:  #7d8799;

    --accept:  #3ddc97;  /* enrolled, ingested — through the gate */
    --pending: #ffc857;  /* discovered, waiting on a human */
    --drop:    #ff6b8b;  /* untrusted, dropped at the outer gate */
    --forge:   #b08cff;  /* conflicting attribution — a forged report */
    --info:    #56b6ff;  /* configuration, links */

    --rail-w: 232px;
    --r: 4px;
    --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
    --prose: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  }

  @media (prefers-color-scheme: light) {
    :root {
      --ground:    #eef1f6;
      --surface:   #ffffff;
      --raised:    #f4f6fa;
      --line:      #d3dae5;
      --line-soft: #e3e8f0;
      --ink:       #111826;
      --ink-dim:   #556074;
      /* 5.0:1 on white; the previous #7b8698 was 3.7:1. */
      --ink-faint: #66707f;

      --accept:  #04794f;
      --pending: #94600a;
      --drop:    #c11e45;
      --forge:   #6236c9;
      --info:    #0a5fbf;
    }
  }

  [data-state="accept"]  { --state: var(--accept); }
  [data-state="pending"] { --state: var(--pending); }
  [data-state="drop"]    { --state: var(--drop); }
  [data-state="forge"]   { --state: var(--forge); }
  [data-state="info"]    { --state: var(--info); }
  [data-state="mute"]    { --state: var(--ink-faint); }
}

@layer base {
  * { box-sizing: border-box; }

  body {
    margin: 0;
    background: var(--ground);
    color: var(--ink);
    /* Monospace is the primary voice here, not the fallback. Nearly every
       value on these screens is a hostname, an origin or a directive. */
    font: 400 13px/1.5 var(--mono);
    -webkit-font-smoothing: antialiased;
  }

  h1, h2, h3 {
    margin: 0;
    font-weight: 600;
    letter-spacing: -0.01em;
  }
  h1 { font-size: 15px; }
  h2 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-dim); }

  a { color: var(--info); text-decoration: none; }
  a:hover { text-decoration: underline; }

  p { margin: 0; }
  .prose { font-family: var(--prose); color: var(--ink-dim); font-size: 13.5px; line-height: 1.55; }

  :focus-visible {
    outline: 2px solid var(--info);
    outline-offset: 2px;
    border-radius: 2px;
  }

  @media (prefers-reduced-motion: reduce) {
    * { transition: none !important; animation: none !important; }
  }
}

@layer shell {
  .shell {
    display: grid;
    grid-template-columns: var(--rail-w) minmax(0, 1fr);
    min-height: 100vh;
  }

  /* ---- sidebar ---- */
  .rail {
    display: flex;
    flex-direction: column;
    gap: 20px;
    padding: 18px 0;
    background: var(--surface);
    border-right: 1px solid var(--line);
    position: sticky;
    top: 0;
    height: 100vh;
  }

  .mark { display: block; padding: 0 18px 4px; color: var(--ink); text-decoration: none; }
  .mark:hover { text-decoration: none; }
  .mark-glyph {
    display: block;
    font-size: 17px;
    font-weight: 700;
    letter-spacing: 0.14em;
  }
  .mark-sub {
    display: block;
    font-size: 10px;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--ink-faint);
  }

  .rail nav { display: flex; flex-direction: column; }

  .nav {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 8px 18px;
    color: var(--ink-dim);
    text-decoration: none;
    border-left: 2px solid transparent;
    transition: background 120ms, color 120ms;
  }
  .nav:hover { background: var(--raised); color: var(--ink); text-decoration: none; }
  .nav.on {
    background: var(--raised);
    color: var(--ink);
    border-left-color: var(--state, var(--info));
  }

  .count {
    min-width: 20px;
    padding: 1px 6px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--state, var(--info)) 18%, transparent);
    color: var(--state, var(--info));
    font-size: 11px;
    font-weight: 600;
    text-align: center;
  }

  .rail-foot { margin-top: auto; padding: 0 18px; display: flex; flex-direction: column; gap: 8px; }
  .stamp { font-size: 11px; letter-spacing: 0.14em; color: var(--ink-dim); text-transform: uppercase; }

  /* Before sign-in there is no navigation, so the rail becomes a header
     strip rather than an empty column. */
  .shell.bare { grid-template-columns: 1fr; }
  .shell.bare .rail {
    position: static;
    height: auto;
    flex-direction: row;
    align-items: baseline;
    gap: 12px;
    padding: 14px 22px;
    border-right: 0;
    border-bottom: 1px solid var(--line);
  }
  .shell.bare .mark { padding: 0; }
  .shell.bare .mark-glyph, .shell.bare .mark-sub { display: inline; }
  .shell.bare .mark-sub { margin-left: 8px; }
  .shell.bare .rail-foot { margin: 0 0 0 auto; padding: 0; flex-direction: row; align-items: baseline; }
  .shell.bare .bar { display: none; }

  /* ---- stage ---- */
  .stage { display: flex; flex-direction: column; min-width: 0; }

  .bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 16px;
    padding: 14px 22px;
    border-bottom: 1px solid var(--line);
    background: var(--surface);
    position: sticky;
    top: 0;
    z-index: 5;
  }

  .body { padding: 18px 22px 64px; display: flex; flex-direction: column; gap: 18px; }

  @media (max-width: 860px) {
    .shell { grid-template-columns: 1fr; }
    .rail { position: static; height: auto; flex-direction: row; align-items: center; flex-wrap: wrap; gap: 10px; padding: 12px 14px; }
    .rail nav { flex-direction: row; flex-wrap: wrap; }
    .nav {
    position: relative; border-left: 0; border-bottom: 2px solid transparent; padding: 6px 10px; }
    .nav.on { border-left: 0; border-bottom-color: var(--state, var(--info)); }
    .rail-foot { margin: 0; padding: 0; flex-direction: row; align-items: center; }
    .body, .bar { padding-inline: 14px; }
  }
}

@layer components {
  /* ---- the gate chain: this product's shape, rendered ---- */
  .gate { display: flex; align-items: stretch; gap: 0; }

  .gate-step {
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 2px 16px;
    border-left: 2px solid var(--state);
    min-width: 84px;
  }
  .gate-step + .gate-step { margin-left: 14px; position: relative; }
  .gate-step + .gate-step::before {
    content: "";
    position: absolute;
    left: -11px;
    top: 50%;
    width: 6px;
    height: 6px;
    border-top: 1px solid var(--line);
    border-right: 1px solid var(--line);
    transform: translateY(-50%) rotate(45deg);
  }
  .gate-n { font-size: 18px; font-weight: 650; line-height: 1; color: var(--state); font-variant-numeric: tabular-nums; }
  .gate-l { font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-dim); }

  /* ---- panels ---- */
  .panel {
    background: var(--surface);
    border: 1px solid var(--line);
    border-left: 2px solid var(--state, var(--line));
    border-radius: var(--r);
    overflow: hidden;
  }
  .panel > header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 10px;
    padding: 10px 14px;
    border-bottom: 1px solid var(--line-soft);
  }
  .panel > .pad { padding: 12px 14px; }
  /* A paragraph dropped straight onto a panel, with no .pad around it, sat
     flush against the border. .empty carries its own padding already. */
  .panel > p:not(.empty) { padding-left: 14px; padding-right: 14px; }
  /* Blocks stacked in a panel body space themselves. `p { margin: 0 }` is right
     for a paragraph in a table cell and wrong for an explanation, a form and a
     footnote in a column — which is how the onboarding panel ended up with its
     fields welded to the sentence above them. Set here so it holds for every
     panel rather than being remembered per page. */
  .pad > * + * { margin-top: 12px; }

  .grid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr)); }

  /* ---- dense tables ---- */
  .table-wrap { overflow-x: auto; }
  table { width: 100%; border-collapse: collapse; }
  thead th {
    position: sticky;
    top: 0;
    text-align: left;
    padding: 7px 14px;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-faint);
    background: var(--surface);
    border-bottom: 1px solid var(--line);
    white-space: nowrap;
  }
  tbody td {
    padding: 7px 14px;
    border-bottom: 1px solid var(--line-soft);
    vertical-align: top;
  }
  tbody tr:hover td { background: var(--raised); }
  tbody tr:last-child td { border-bottom: 0; }
  td.num { text-align: right; font-variant-numeric: tabular-nums; }
  td.dim, .dim { color: var(--ink-dim); }

  /* A cell holds its line by default.
   *
   * `fill` below gives one column all the leftover width, so every other
   * column shrinks to its min-content — and min-content breaks at a hyphen.
   * `script-src-elem` came out as "script-" over "src-elem", and since almost
   * every directive in this product is hyphenated, that doubled the height of
   * every row in the triage table while the table itself had width to spare.
   * "8m ago" was breaking after "8m".
   *
   * The columns that carry unbounded text — a URL, an origin, a policy, a
   * message — opt back in with `wrap`, and .table-wrap scrolls if the total
   * still does not fit. Better a scrollbar on one table than every row in the
   * product two lines tall. */
  tbody td { white-space: nowrap; }
  /* The floor matters as much as the wrapping. `wrap` allows a break anywhere,
     which is right for a URL with no spaces in it and wrong without a minimum:
     a `fill` column next door takes all the leftover width and starves this one
     to its min-content, which is one character. The audit log's subject was
     coming out as "localh" over "ost". */
  td.wrap { white-space: normal; overflow-wrap: anywhere; min-width: 11rem; }
  td.rail-cell { border-left: 2px solid var(--state, transparent); }
  /* One column absorbs the leftover width so the rest shrink to their content,
     instead of every column being stretched to an equal share. */
  th.fill { width: 100%; }
  th.act, td.act { width: 1%; white-space: nowrap; text-align: right; }

  /* ---- state pills ---- */
  .pill {
    display: inline-block;
    padding: 1px 7px;
    border-radius: 999px;
    border: 1px solid color-mix(in srgb, var(--state, var(--ink-faint)) 45%, transparent);
    background: color-mix(in srgb, var(--state, var(--ink-faint)) 14%, transparent);
    color: var(--state, var(--ink-dim));
    font-size: 11px;
    white-space: nowrap;
  }

  .chip {
    display: inline-block;
    padding: 2px 8px;
    border-radius: var(--r);
    font-size: 10px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    border: 1px solid var(--line);
    color: var(--ink-dim);
  }
  .chip.warn { border-color: color-mix(in srgb, var(--pending) 50%, transparent); color: var(--pending); }
  .chip.warn:hover { text-decoration: none; background: color-mix(in srgb, var(--pending) 12%, transparent); }

  /* ---- flashes ---- */
  .flash {
    margin: 0;
    padding: 9px 22px;
    font-size: 12px;
    border-bottom: 1px solid var(--line);
    border-left: 2px solid var(--state);
    background: color-mix(in srgb, var(--state) 10%, transparent);
    color: var(--ink);
  }
  .flash.ok   { --state: var(--accept); }
  .flash.bad  { --state: var(--drop); }
  .flash.warn { --state: var(--pending); }

  .chip.bad { border-color: color-mix(in srgb, var(--drop) 55%, transparent); color: var(--drop); }

  .signed-in { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  .who { font-size: 11px; color: var(--ink-dim); overflow-wrap: anywhere; }

  /* The sign-in page is the one screen with no navigation to speak of. */
  .panel.signin { max-width: 26rem; margin: 6vh auto 0; }
  .entra {
    display: inline-block;
    padding: 9px 16px;
    border: 1px solid var(--info);
    border-radius: var(--r);
    background: color-mix(in srgb, var(--info) 16%, transparent);
    color: var(--info);
    font-weight: 600;
  }
  .entra:hover { background: color-mix(in srgb, var(--info) 28%, transparent); text-decoration: none; }

  /* ---- empty states: an invitation, not an apology ---- */
  .empty { padding: 22px 14px; color: var(--ink-dim); font-family: var(--prose); font-size: 13.5px; }

  /* ---- forms ---- */
  /* A form row wraps, and anything tall takes a line of its own.
     A textarea sitting inline with 11rem inputs stretches the row to its own
     height and carries its label up out of view with it — which is how the
     webhook editor ended up with an unlabelled box. */
  .fields { display: flex; flex-wrap: wrap; gap: 12px 14px; align-items: end; }
  label { display: flex; flex-direction: column; gap: 4px; font-size: 11px; letter-spacing: 0.07em; text-transform: uppercase; color: var(--ink-dim); }
  label.inline { flex-direction: row; align-items: center; gap: 6px; text-transform: none; letter-spacing: 0; font-size: 12px; color: var(--ink-dim); }

  /* One control style, applied to every control. A native <select> beside a
     styled <input> is the detail that makes a form look half-finished.
   *
   * The selector names what must NOT be styled, rather than listing what must.
   * An allow-list is wrong by construction here and was: it read
   * `input[type="text"], input:not([type])`, so `type="url"` on the onboarding
   * form and `type="password"` on the sign-in page fell straight through to the
   * browser's default control — different font, different height, and a box too
   * narrow to read the value back. Nobody writing a new field would think to
   * come and add its type. Written this way, the next input type is covered
   * without anyone remembering anything.
   *
   * :where() holds the specificity at 0-0-1 so the width modifiers further down
   * still win. */
  input:where(:not(
    [type="checkbox"], [type="radio"], [type="file"], [type="range"],
    [type="color"], [type="submit"], [type="reset"], [type="image"],
    [type="button"], [type="hidden"]
  )), select, textarea {
    font: inherit;
    font-size: 12px;
    padding: 6px 8px;
    min-width: 11rem;
    color: var(--ink);
    background: var(--ground);
    border: 1px solid var(--line);
    border-radius: var(--r);
  }
  /* An input carrying `size` has already said how wide it is, and the 11rem
     floor above is for the ones that have not. Without this the noise rules'
     trial window — `size="5"`, holding "168h" — still came out 11rem wide,
     pushed its two buttons onto a second line, and made every row in a
     thirteen-rule table three lines tall. The author did the right thing in the
     markup and the stylesheet ignored it. */
  input[size] { min-width: 0; width: auto; }

  select { padding-right: 26px; cursor: pointer; }
  input::placeholder, textarea::placeholder { color: var(--ink-faint); opacity: 1; }
  input:focus { border-color: var(--info); }

  button {
    font: 600 12px/1 var(--mono);
    letter-spacing: 0.04em;
    padding: 7px 12px;
    border-radius: var(--r);
    border: 1px solid var(--accept);
    background: color-mix(in srgb, var(--accept) 16%, transparent);
    color: var(--accept);
    cursor: pointer;
    transition: background 120ms;
  }
  button:hover { background: color-mix(in srgb, var(--accept) 28%, transparent); }
  button.ghost { border-color: var(--line); background: transparent; color: var(--ink-dim); }
  button.ghost:hover { background: var(--raised); color: var(--ink); }

  .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
  /* …except inside a table cell. `td.act` is `width: 1%`, so the column takes
     its min-content width, and the min-content of a *wrapping* flex container
     is its widest single item — which puts every button on its own line and
     grows the row by a line per action. The action column is meant to be the
     narrow one; it cannot also be the one that wraps. */
  td .row { flex-wrap: nowrap; }

  /* ---- specimens: attacker-reachable text, rendered as inert text ---- */
  details > summary { cursor: pointer; color: var(--ink-dim); font-size: 12px; padding: 8px 14px; }
  details[open] > summary { border-bottom: 1px solid var(--line-soft); }
  pre.specimen {
    margin: 0;
    padding: 10px 14px;
    background: var(--ground);
    border-top: 1px solid var(--line-soft);
    color: var(--ink-dim);
    font-size: 11.5px;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    max-height: 15rem;
    overflow: auto;
  }

  /* ---- one pending origin: three lines, not a page of prose ---- */
  .origin { padding: 11px 14px; border-bottom: 1px solid var(--line-soft); }
  .origin:last-child { border-bottom: 0; }
  .origin-head { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; margin-bottom: 9px; }
  .origin-name { font-size: 14px; font-weight: 600; overflow-wrap: anywhere; }
  /* The dismissal row sits well clear of the enroll fields above it: the
     project input has a datalist, and the browser's native dropdown opens
     downward over whatever is directly beneath. */
  .origin-foot { margin-top: 18px; font-size: 11px; align-items: center; }
  /* The explanation wraps to its own line rather than being clipped by the
     buttons it belongs to. */
  .origin-foot .dim { flex: 1 1 20rem; }
  .origin details { margin-top: 9px; border-top: 1px solid var(--line-soft); }
  .origin details > summary { padding: 8px 0 0; }
  .origin pre.specimen { margin-top: 8px; border: 1px solid var(--line-soft); border-radius: var(--r); }

  /* ---- generated snippets (§3.3) ---- */
  .snippet { position: relative; }
  .snippet + .snippet pre.specimen { border-top: 0; }
  /* The button floats over the top-right of the block. The pre reserves room
     for it so a long first line never runs underneath. */
  .snippet .copy { position: absolute; top: 7px; right: 10px; z-index: 1; font-size: 11px; }
  .snippet pre.specimen { padding-right: 74px; max-height: 26rem; }

  /* ---- the endpoint test's chain of steps (§3.3) ---- */
  ol.chain { list-style: none; margin: 0; padding: 0 14px 14px; }
  ol.chain li {
    display: grid;
    grid-template-columns: 1.4rem minmax(9rem, auto) 1fr;
    gap: 4px 12px;
    align-items: baseline;
    padding: 9px 0;
    border-top: 1px solid var(--line-soft);
  }
  ol.chain .chain-mark { font-weight: 700; text-align: center; }
  ol.chain .chain-name { font-weight: 600; font-size: 12.5px; }
  ol.chain .chain-detail { color: var(--ink-dim); font-size: 12px; overflow-wrap: anywhere; }
  ol.chain li[data-state="ok"]   .chain-mark { color: var(--accept); }
  ol.chain li[data-state="fail"] .chain-mark { color: var(--drop); }
  ol.chain li[data-state="warn"] .chain-mark { color: var(--pending); }
  ol.chain li[data-state="skip"] .chain-mark { color: var(--ink-faint); }
  /* Narrow screens: the label and detail stack under the mark rather than
     squeezing the detail into a column too thin to read. */
  @media (max-width: 40rem) {
    ol.chain li { grid-template-columns: 1.4rem 1fr; }
    ol.chain .chain-detail { grid-column: 2; }
  }

  /* A checkbox sits on the field baseline rather than above it: .fields
     stacks a label over its control, which is wrong for a control that is
     beside its text. */
  label.check { flex-direction: row; align-items: center; gap: 6px; align-self: end; padding-bottom: 6px; }
  label.check input { margin: 0; }

  /* A sub-heading inside a panel that carries two related controls. */
  h3.sub {
    font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em;
    color: var(--ink-dim); margin: 0 0 8px;
  }

  /* A tag on a group row doubles as a filter link (§7.4). */
  .pill.tag { text-decoration: none; margin-left: 4px; }
  .pill.tag:hover { text-decoration: underline; }

  dl.facts { display: flex; flex-wrap: wrap; gap: 4px 26px; margin: 0; }
  dl.facts div { display: flex; gap: 8px; align-items: baseline; }
  dl.facts dt { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-faint); }
  dl.facts dd { margin: 0; color: var(--ink); font-variant-numeric: tabular-nums; }
}

@layer components {
  /* A panel that wants the full stage width even inside a grid. */
  .wide { grid-column: 1 / -1; }

  /* Segmented control: window and environment pickers. Links, not JS —
     filtering is server-side because that is where the data lives (§17.4). */
  .seg { display: inline-flex; border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; }
  .seg a {
    padding: 4px 10px; font-size: 11px; color: var(--ink-dim);
    text-decoration: none; border-right: 1px solid var(--line); white-space: nowrap;
  }
  .seg a:last-child { border-right: 0; }
  .seg a:hover { background: var(--raised); color: var(--ink); text-decoration: none; }
  .seg a.on { background: color-mix(in srgb, var(--info) 18%, transparent); color: var(--info); }

  /* Charts are inline SVG rendered by the server. A CDN library is blocked by
     the dashboard's own CSP and most of them assign innerHTML, which
     require-trusted-types-for makes throw (§11). */
  .chart { margin: 0; position: relative; padding-left: 34px; }
  .chart svg { width: 100%; height: 132px; display: block; overflow: visible; }
  .chart .grid { stroke: var(--line-soft); stroke-width: 1; vector-effect: non-scaling-stroke; }
  .chart .b-enforce { fill: var(--drop); }
  .chart .b-report  { fill: color-mix(in srgb, var(--pending) 72%, transparent); }
  .chart g:hover rect { fill: var(--info); }
  .chart figcaption {
    margin-top: 8px; font-size: 11px; color: var(--ink-faint);
    display: flex; gap: 8px; align-items: center;
  }
  .yaxis { position: absolute; left: 0; top: 0; height: 132px; width: 30px; }
  .yaxis span {
    position: absolute; top: var(--y); right: 0; transform: translateY(-50%);
    font-size: 9px; color: var(--ink-faint); font-variant-numeric: tabular-nums;
  }

  .legend { margin: 10px 0 0; font-size: 11px; color: var(--ink-dim); display: flex; gap: 14px; align-items: center; flex-wrap: wrap; }
  .key { width: 9px; height: 9px; border-radius: 2px; background: var(--state); display: inline-block; }

  /* The estimate marker. A count that was extrapolated from a sample must
     never render as if it were counted (§7.8). */
  .est { color: var(--pending); margin-left: 2px; cursor: help; }

  .tiny { padding: 1px 5px; font-size: 11px; line-height: 1; }
}

@layer components {
  /* Forms that only exist to be targeted by a `form=` attribute elsewhere.
     HTML forbids nesting forms, so the per-origin actions live outside the
     bulk-select form and are wired to it by id. */
  .hidden { display: none; }
  .bulk { display: inline-flex; gap: 6px; align-items: center; }
  .bulk input { max-width: 15rem; }
}

@layer components {
  .envchart { margin-bottom: 16px; }
  .envchart h3 {
    margin: 0 0 6px; font-size: 11px; letter-spacing: 0.09em;
    text-transform: uppercase; color: var(--ink-dim);
  }
  /* Deploy markers. A vertical rule rather than a label: several releases a
     day is normal here, and labelled markers would be unreadable. The title
     carries the ref. */
  .chart .marker line { stroke: var(--info); stroke-width: 1; stroke-dasharray: 3 3; }
  .chart .marker:hover line { stroke-width: 2; stroke-dasharray: none; }

  .bulkbar { border-top: 1px solid var(--line-soft); }
  .bulkbar .dim { margin: 8px 0 0; font-family: var(--prose); font-size: 12px; }
  /* Field width is a property of what the field holds, so there are three
     sizes and no ad-hoc widths in page CSS. The default 11rem suits a slug, a
     count or an environment name. `long` is for a URL, an origin or a pattern:
     it shares the row, grows into the space, and wraps to its own line rather
     than clipping — a value you cannot read back is a value nobody checks.
     `grow` takes a full line, for prose and payloads. */
  .fields label.long { flex: 1 1 24rem; min-width: 0; }
  .fields label.long input { width: 100%; }
  .fields label.grow { flex: 1 1 100%; }
  .fields label.grow input,
  .fields label.grow textarea { width: 100%; }
  .fields label .hint { text-transform: none; letter-spacing: 0; color: var(--ink-faint); }

  /* Buttons belong together at the end of a form, not scattered between the
     fields they act on. */
  .fields .actions { display: flex; gap: 8px; flex-wrap: wrap; margin-left: auto; }

  /* Visually hidden, still announced. */
  .vh {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }
}

@layer components {
  /* Per-project section tabs. A project has five screens and no other way to
     move between them; the sidebar is platform-level. */
  /* Five links that all look identical tell you nothing about where you are.
     The current section is marked, and marked with the same left-edge accent
     the panels use rather than a second visual language. */
  .tabs {
    display: flex; gap: 2px; flex-wrap: wrap;
    margin: 0 0 4px; border-bottom: 1px solid var(--line-soft);
  }
  .tabs a {
    padding: 7px 13px; font-size: 12px; color: var(--ink-dim);
    text-decoration: none; border-bottom: 2px solid transparent;
    margin-bottom: -1px;
  }
  .tabs a:hover { color: var(--ink); text-decoration: none; border-bottom-color: var(--line); }
  .tabs a.on {
    color: var(--ink); border-bottom-color: var(--info);
    background: color-mix(in srgb, var(--info) 10%, transparent);
  }
  /* Indentation means exactly one thing here: a sub-view of the entry above.
     Policy posture is /fleet/policy and belongs under Fleet. Domains and Users
     are siblings of everything else and were briefly indented under an "Admin"
     heading, which made Domains read as part of Users — a hierarchy that does
     not exist. */
  .nav.sub {
    padding-left: 34px; font-size: 12px;
    color: var(--ink-dim);
  }
  .nav.sub::before {
    content: ""; position: absolute; left: 20px; top: 50%;
    width: 7px; height: 1px; background: var(--line);
  }
  .verdict { font-family: var(--prose); font-size: 14px; margin: 0 0 8px; }
  code { font-family: var(--mono); font-size: 0.92em; color: var(--info); }
}

@layer components {
  .count.alert { background: color-mix(in srgb, var(--drop) 22%, transparent); color: var(--drop); }
  tr.unread td { font-weight: 500; }
  textarea { font-family: var(--mono); line-height: 1.5; resize: vertical; min-height: 3.5rem; }
  h3, h4 { font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-dim); margin: 14px 0 6px; }
}

@layer components {
  /* The themed replacement for a native <datalist> popup, which is browser
     chrome and unreachable from CSS. combo.js swaps one for the other; without
     it the native list still works, just in the platform's own colours. */
  .combo { position: relative; display: flex; flex-direction: column; }
  .combo-list {
    position: absolute; top: calc(100% + 4px); left: 0; z-index: 20;
    margin: 0; padding: 4px; list-style: none;
    min-width: 100%; max-width: 22rem; max-height: 15rem; overflow-y: auto;
    background: var(--raised);
    border: 1px solid var(--line);
    border-radius: var(--r);
    box-shadow: 0 8px 24px rgb(0 0 0 / 0.45);
  }
  .combo-item {
    display: flex; align-items: baseline; gap: 8px;
    padding: 6px 8px; border-radius: 3px; cursor: pointer;
    font-size: 12px; color: var(--ink);
  }
  .combo-item b { font-weight: 600; }
  .combo-item span { color: var(--ink-dim); font-size: 11px; overflow-wrap: anywhere; }
  .combo-item:hover, .combo-item.on { background: color-mix(in srgb, var(--info) 20%, transparent); }
  .combo-item.on { outline: 1px solid color-mix(in srgb, var(--info) 45%, transparent); }
}

@layer components {
  /* Destructive actions look destructive. Used only where the action cannot
     be undone: removing a trusted domain, deleting a project. */
  button.danger {
    border-color: color-mix(in srgb, var(--drop) 55%, transparent);
    background: color-mix(in srgb, var(--drop) 14%, transparent);
    color: var(--drop);
  }
  button.danger:hover { background: color-mix(in srgb, var(--drop) 26%, transparent); }
  .confirm-why { max-width: 32rem; text-align: left; white-space: normal; font-family: var(--prose); }
}

@layer components {
  /* Initials, not a photograph. The dashboard serves img-src 'self' data:
     (§11), so a remote avatar would mean loosening that or proxying and
     caching somebody's picture — a privacy decision, not a styling one. */
  .avatar {
    display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 26px; border-radius: 50%;
    font-size: 10px; font-weight: 650; letter-spacing: 0.04em;
    background: color-mix(in srgb, var(--state, var(--info)) 22%, transparent);
    color: var(--state, var(--info));
    border: 1px solid color-mix(in srgb, var(--state, var(--info)) 40%, transparent);
  }
}

@layer components {
  /* The project name is the way into a project, so it should look like one
     without borrowing the link colour used for actions. */
  .project-link { color: var(--ink); text-decoration: none; }
  .project-link:hover { color: var(--info); text-decoration: underline; }
}

  /* ---------------------------------------------------------------- history
     A policy timeline. The unit of attention is the CHANGE, not the policy: a
     list of hashes answers a question nobody asks. So each entry leads with what
     moved, and the rail carries the verdict in colour — a weakening in an
     enforcing environment is the one thing here that must be impossible to
     scroll past.

     Built on the same --state token every other component uses, so a weakening
     reads as the same red as a dropped report elsewhere. */
  ol.timeline { list-style: none; margin: 0; padding: 0 14px 14px; }
  ol.timeline > li {
    display: grid;
    grid-template-columns: 0.9rem 1fr;
    gap: 0 14px;
    position: relative;
    padding: 0 0 4px;
  }
  /* The rail itself, drawn between the nodes rather than as a border, so the
     last entry does not trail a line into nothing. */
  ol.timeline > li::before {
    content: "";
    grid-column: 1;
    justify-self: center;
    width: 2px;
    background: var(--line);
    grid-row: 1 / -1;
  }
  ol.timeline > li:last-child::before { background: linear-gradient(var(--line), transparent); }
  ol.timeline .node {
    grid-column: 1;
    grid-row: 1;
    justify-self: center;
    width: 11px; height: 11px;
    margin-top: 5px;
    border-radius: 50%;
    background: var(--ground);
    border: 2px solid var(--state, var(--ink-faint));
    z-index: 1;
  }
  ol.timeline > li[data-state="drop"] .node { background: var(--state); }
  ol.timeline .entry { grid-column: 2; padding: 0 0 18px; min-width: 0; }

  /* The headline: date, then the change in words. Sized so the change reads
     first and the metadata recedes. */
  .entry-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
  .entry-when { font-family: var(--mono); font-size: 12px; color: var(--ink-dim); }
  .entry-what { font-weight: 600; font-size: 13.5px; }
  .entry-meta { font-size: 12px; color: var(--ink-faint); font-family: var(--mono); }

  /* One line per directive that moved. The arrow is the point: before on the
     left, after on the right, and no table chrome between them. */
  ul.moves { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 5px; }
  ul.moves li {
    display: grid;
    grid-template-columns: minmax(7rem, auto) 1fr;
    gap: 2px 10px;
    font-size: 12px;
    padding: 5px 9px;
    border-radius: var(--r);
    background: var(--raised);
    border-left: 2px solid var(--state, var(--line));
  }
  ul.moves .move-dir { font-family: var(--mono); font-weight: 600; }
  ul.moves .move-val { font-family: var(--mono); overflow-wrap: anywhere; color: var(--ink-dim); }
  ul.moves .move-val ins { text-decoration: none; color: var(--accept); }
  ul.moves .move-val del { text-decoration: line-through; color: var(--drop); opacity: 0.75; }
  @media (max-width: 40rem) {
    ul.moves li { grid-template-columns: 1fr; }
  }
