/* =====================================================================================
   Tabler bridge — ADR 0006.

   Tabler is the shell now, but it ships a cold blue-grey palette, and this product spent
   two design rounds getting away from exactly that ("erg wit" → "jaren 90" → "alsof ik bij
   de tandarts ben", 2026-08-21/22 / andre). So Tabler does not get to bring its own colours:
   this file re-points every --tblr-* surface, ink, edge and accent at the warm tokens that
   survive from the instrument design, and Tabler inherits them.

   Load order matters — this file comes AFTER tabler.min.css, or Tabler's own :root wins.

   Why the mapping is written twice, once per data-bs-theme: our tokens already follow the
   OS (prefers-color-scheme) and the explicit [data-theme] stamp. Pointing BOTH of Tabler's
   theme blocks at the same token names means whichever one Tabler thinks is active resolves
   to the right value, and light/dark keeps working with no JavaScript and no flash. The one
   exception is the sidebar, which is deliberately pinned to fixed dark values below.
   ===================================================================================== */

:root,
[data-bs-theme="light"],
[data-bs-theme="dark"] {
  /* Type — Inter and JetBrains Mono are already self-hosted by style.css */
  --tblr-font-sans-serif: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --tblr-font-monospace:  "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
  --tblr-body-font-size: .875rem;
  --tblr-body-line-height: 1.55;

  /* Surfaces. The page is a plane; cards sit on it. Same ladder as the instrument design. */
  --tblr-body-bg:              var(--plane);
  --tblr-body-bg-rgb:          239, 236, 231;
  --tblr-bg-surface:           var(--canvas);
  --tblr-bg-surface-secondary: var(--anodised);
  --tblr-bg-surface-tertiary:  var(--anodised-deep);
  --tblr-bg-surface-dark:      var(--tape);
  --tblr-card-bg:              var(--canvas);
  --tblr-card-cap-bg:          var(--canvas);
  --tblr-modal-bg:             var(--canvas);
  --tblr-dropdown-bg:          var(--raised);
  --tblr-tertiary-bg:          var(--anodised);

  /* Ink — four levels, because two is a flat hierarchy */
  --tblr-body-color:          var(--chart-ink);
  --tblr-body-color-rgb:      28, 26, 23;
  --tblr-emphasis-color:      var(--chart-ink);
  --tblr-secondary-color:     var(--ink-2);
  --tblr-secondary:           var(--ink-2);
  --tblr-tertiary-color:      var(--ink-3);
  --tblr-muted:               var(--ink-3);
  --tblr-heading-color:       var(--chart-ink);

  /* Edges — findable, never the first thing you see */
  --tblr-border-color:            var(--edge);
  --tblr-border-color-translucent: var(--edge-soft);
  --tblr-card-border-color:       var(--edge-soft);
  --tblr-border-width: 1px;

  /* One accent: the teal of a plotter at night. Structure is grey; colour communicates. */
  --tblr-primary:        var(--plotter);
  --tblr-primary-rgb:    15, 90, 102;
  --tblr-primary-fg:     #ffffff;
  --tblr-link-color:     var(--plotter);
  --tblr-link-color-rgb: 15, 90, 102;
  --tblr-link-hover-color: var(--plotter-lift);
  --tblr-active-bg:      var(--anodised);

  /* State is reserved and never reused for identity. These four hexes are the validated
     status scale — contrast was measured, not eyeballed. They do not move. */
  --tblr-success:     var(--led-good);
  --tblr-success-rgb: 28, 107, 58;
  --tblr-warning:     var(--annunciator);
  --tblr-warning-rgb: 138, 90, 0;
  --tblr-danger:      var(--estop);
  --tblr-danger-rgb:  163, 22, 27;
  --tblr-info:        var(--plotter);

  /* Concentric radii: a control inside a container is one step smaller, never the same. */
  --tblr-border-radius:    var(--radius-s);
  --tblr-border-radius-sm: 6px;
  --tblr-border-radius-lg: var(--radius-m);
  --tblr-border-radius-xl: var(--radius-l);

  --tblr-box-shadow-card: var(--lift-sm);
  --tblr-box-shadow:      var(--lift);
}

/* The sidebar sits on the same ground as the page, and marks the current item with a quiet
   fill plus an accent edge. Adopted from the reference on 2026-08-22 (styleguide §06): a rail
   in a different colour splits the screen into "sidebar world" and "content world", and on a
   tool that is mostly quiet data the calmer read wins. It also follows the OS theme now,
   instead of being pinned dark — one ground, both themes. */
.navbar-vertical {
  --tblr-body-bg:        var(--plane);
  --tblr-bg-surface:     var(--plane);
  --tblr-body-color:     var(--ink-2);
  --tblr-secondary-color:var(--ink-3);
  --tblr-emphasis-color: var(--chart-ink);
  --tblr-border-color:   var(--edge-soft);
  --tblr-active-bg:      var(--anodised-deep);
  background: var(--plane);
  border-right: 1px solid var(--edge-soft);
}

/* Where am I — the first question navigation has to answer. A left bar and full-strength
   ink, not a filled pill: this is an instrument, and a filled shape would be the loudest
   thing on a screen full of quiet data. */
.navbar-vertical .nav-link { border-left: 2px solid transparent; padding-left: .75rem; }
.navbar-vertical .nav-link:hover { color: var(--tblr-emphasis-color); }
.navbar-vertical .nav-item.active > .nav-link,
.navbar-vertical .nav-link[aria-current="page"] {
  color: var(--tblr-emphasis-color);
  font-weight: 600;
  border-left-color: var(--tblr-primary);
  background: var(--tblr-active-bg);
}
.navbar-vertical .navbar-brand {
  font-weight: 620; letter-spacing: -0.015em; font-size: 1rem;
  color: var(--tblr-emphasis-color);
}
.navbar-vertical .nav-section-title {
  font-size: .68rem; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink-4); padding: 1rem .75rem .25rem; font-weight: 600;
}
.navbar-vertical .nav-link { border-radius: 0 var(--radius-s) var(--radius-s) 0; margin-right: .5rem; }

/* Page chrome. The header is a translucent layer content passes under, not an opaque strip
   that owns a band of the screen (apple-design §12). */
.page-header { padding-top: 1.25rem; padding-bottom: .25rem; }
.page-pretitle { color: var(--ink-3); letter-spacing: .07em; font-size: .68rem; }
.page-title { font-size: 1.35rem; font-weight: 620; letter-spacing: -0.02em; }
.card { box-shadow: var(--lift-sm); }
.card-title { font-size: .95rem; font-weight: 600; letter-spacing: -0.01em; }

/* Tabler's default is a heavier, rounder button than this product's. Match the instrument. */
.btn { font-weight: 500; }
/* The reference's primary control is near-black, not its green — which is exactly why its
   accent never competes with its data. Copied deliberately. */
.btn-primary { --tblr-btn-bg: var(--action); --tblr-btn-border-color: var(--action);
               --tblr-btn-color: var(--canvas);
               --tblr-btn-hover-bg: var(--action-lift); --tblr-btn-hover-border-color: var(--action-lift);
               --tblr-btn-hover-color: var(--canvas); }

/* Numbers line up in columns, everywhere, always. */
.tabular, .card .h1, .grid-host { font-variant-numeric: tabular-nums; }

@media print {
  .navbar-vertical, .page-header .btn, .no-print { display: none !important; }
  .page-wrapper { margin-left: 0 !important; }
  .card { box-shadow: none; border: 1px solid #999; break-inside: avoid; }
}

/* =====================================================================================
   The grid — ADR 0006.

   AG Grid v33 exposes its theme as --ag-* custom properties, so it is bound to this
   product's tokens in CSS rather than through a JavaScript theme object. One consequence
   worth having: light and dark follow the same tokens as everything else, with no theme
   detection, no flash, and no second source of truth for a colour.
   ===================================================================================== */

.grid-host {
  --ag-background-color:            var(--canvas);
  --ag-foreground-color:            var(--chart-ink);
  --ag-header-background-color:     var(--anodised);
  --ag-header-foreground-color:     var(--ink-3);
  --ag-border-color:                var(--edge-soft);
  --ag-header-column-border:        none;
  --ag-row-border-color:            var(--edge-soft);
  --ag-row-hover-color:             var(--anodised);
  --ag-selected-row-background-color: var(--anodised-deep);
  --ag-odd-row-background-color:    transparent;
  --ag-input-border-color:          var(--edge);
  --ag-input-background-color:      var(--inset);
  --ag-accent-color:                var(--plotter);
  --ag-font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
  --ag-font-size: 13px;
  --ag-header-font-size: 11px;
  --ag-header-font-weight: 600;
  --ag-cell-horizontal-padding: 10px;
  --ag-border-radius: 6px;
  --ag-wrapper-border-radius: 0;
}

/* Column headers as small caps: they label, they do not compete with the data. */
.grid-host .ag-header-cell-text { text-transform: uppercase; letter-spacing: .06em; }

/* A row you can click should say so before it is clicked. */
.grid-host .rb-row-link .ag-cell { cursor: pointer; }

/* An identifier, version or date: the same typeface as everything else, with tabular figures
   doing the aligning. The reference never puts a second face in a data cell — a monospace
   column reads as a terminal pasted into an interface, and that mismatch is what the owner
   saw on 2026-08-22. Slightly tight tracking keeps a code like DEMO-8005 compact. */
.grid-host .rb-ident {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "ss01";      /* Inter's disambiguated l/I, for a code read aloud */
  letter-spacing: -.004em;
}
/* Genuine monospace, kept only where the characters themselves are the subject. */
.grid-host .rb-mono { font-family: "JetBrains Mono", ui-monospace, Menlo, monospace; font-size: 12px; }
.grid-host .rb-num  { font-variant-numeric: tabular-nums; }

/* The anchor as label tape — printed white on black, like a Brady label on a cable. */
.rb-tape {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11px; letter-spacing: -0.01em;
  background: var(--tape); color: var(--tape-ink);
  padding: 2px 6px; border-radius: 4px; white-space: nowrap;
}

/* The validated status scale. These pairings were contrast-checked, not eyeballed; the
   word is always present, because colour alone is not a label. */
.rb-pill {
  font-size: 11px; font-weight: 550; letter-spacing: .03em;
  padding: 2px 8px; border-radius: 999px; white-space: nowrap;
  border: 1px solid transparent;
}
.rb-pill-published, .rb-pill-deployed, .rb-pill-granted,
.rb-pill-resolved, .rb-pill-closed, .rb-pill-low {
  background: var(--led-good-bg); color: var(--led-good); border-color: color-mix(in srgb, var(--led-good) 22%, transparent);
}
.rb-pill-submitted, .rb-pill-pending, .rb-pill-planned, .rb-pill-medium {
  background: var(--annunciator-bg); color: var(--annunciator); border-color: color-mix(in srgb, var(--annunciator) 22%, transparent);
}
.rb-pill-open, .rb-pill-critical, .rb-pill-high,
.rb-pill-rolled-back, .rb-pill-declined {
  background: var(--estop-bg); color: var(--estop); border-color: color-mix(in srgb, var(--estop) 22%, transparent);
}
.rb-pill-draft, .rb-pill-superseded, .rb-pill-withdrawn, .rb-pill-suspended {
  background: var(--anodised-deep); color: var(--ink-2); border-color: var(--edge);
}

/* The toolbar above a grid is chrome, not data — it stays quiet until reached for. */
.grid-host + .no-print, .card-body.border-bottom .btn { --tblr-btn-color: var(--ink-2); }

@media print {
  /* AG Grid virtualises rows: what is off screen is not in the DOM, so it cannot print.
     Say so on paper rather than emitting a half table that looks complete. */
  .grid-host { display: none !important; }
  .grid-host::after { content: ""; }
  .card:has(.grid-host)::after {
    display: block; padding: 1rem; font-size: 11pt;
    content: "This table is interactive and does not print. Use the “Printable table” link on screen.";
  }
}

/* The floating filter's menu button eats a third of a narrow column and duplicates what the
   header menu already offers — the input itself is the filter. Reclaim the width. */
.grid-host .ag-floating-filter-button { display: none; }
.grid-host .ag-floating-filter-body { width: 100%; }
.grid-host .ag-floating-filter-input .ag-input-field-input { font-size: 12px; }

/* =====================================================================================
   Dashed separators — adopted 2026-08-22 (styleguide §06).

   A dashed hairline carries the row without drawing a grid. On a dense compliance table
   that is the difference between readable and ruled, and it is what the reference does
   behind its charts as well as inside its tables.
   ===================================================================================== */

.grid-host .ag-row { border-bottom: 1px dashed var(--edge); }
.grid-host .ag-row:last-child { border-bottom: 0; }
.grid-host { --ag-row-border-color: transparent; }

.table td, .table th { border-top-style: dashed !important; }
.card-table tbody tr td { border-top: 1px dashed var(--edge); }

/* =====================================================================================
   The delta pill — adopted 2026-08-22 (styleguide §06).

   Arrow plus number in a tinted pill, next to the figure it belongs to. It uses the status
   palette that already exists, and it always carries the arrow AND the sign — colour alone
   is not a direction, and roughly one man in twelve cannot separate this palette by hue.
   ===================================================================================== */

.rb-delta {
  font-size: 11px; font-weight: 600; letter-spacing: .02em;
  padding: 2px 7px; border-radius: 999px; white-space: nowrap;
  display: inline-flex; align-items: center; gap: .2rem;
  border: 1px solid transparent; vertical-align: middle;
}
.rb-delta-up   { background: var(--led-good-bg);   color: var(--led-good);
                 border-color: color-mix(in srgb, var(--led-good) 22%, transparent); }
.rb-delta-down { background: var(--estop-bg);      color: var(--estop);
                 border-color: color-mix(in srgb, var(--estop) 22%, transparent); }
.rb-delta-warn { background: var(--annunciator-bg); color: var(--annunciator);
                 border-color: color-mix(in srgb, var(--annunciator) 22%, transparent); }
.rb-delta-flat { background: var(--anodised-deep);  color: var(--ink-3); border-color: var(--edge); }


/* =====================================================================================
   The in-column filter field.

   Reported 2026-08-22: "de velden waar je in kan typen om te filteren voelen beetje raar".
   They were right — an empty pill with a big radius and no placeholder reads as a broken
   input, not as a filter. AG Grid's default gives it button-sized padding and the same
   corner radius as a card, so a row of them looks like a row of empty buttons.

   Fixed by making it look like what it is: a small inset field, one step smaller in radius
   than the container (concentric), with a hint of what to type. It recedes until reached for.
   ===================================================================================== */

.grid-host .ag-floating-filter { padding-top: 2px; padding-bottom: 4px; }
/* The visible box is AG Grid's wrapper, not the input inside it — styling only the input
   leaves the tall rounded shell that caused the complaint. */
.grid-host .ag-floating-filter .ag-wrapper,
.grid-host .ag-floating-filter .ag-input-wrapper {
  height: 26px; min-height: 26px;
  border-radius: 6px;
  border: 1px solid var(--edge);
  background: var(--inset);
  overflow: hidden;
}
.grid-host .ag-floating-filter .ag-wrapper:focus-within {
  border-color: var(--plotter);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--plotter) 16%, transparent);
  background: var(--canvas);
}
.grid-host .ag-floating-filter .ag-input-field-input,
.grid-host .ag-floating-filter input {
  height: 24px; min-height: 24px;
  padding: 0 8px;
  border: 0; border-radius: 0; background: transparent;
  font-size: 12px; color: var(--chart-ink);
}
.grid-host .ag-floating-filter input::placeholder { color: var(--ink-4); font-size: 11.5px; }
/* outline:none is only allowed because the WRAPPER carries the visible focus — a
   keyboard user tabbing through filter fields must see where they are (WIG-audit
   2026-08-25: this was a bare outline:none, the one real a11y bug of the audit). */
.grid-host .ag-floating-filter input:focus { outline: none; }
.grid-host .ag-floating-filter .ag-wrapper:focus-within {
  border-color: var(--edge-strong);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--plotter) 18%, transparent);
}
.grid-host .ag-floating-filter .ag-wrapper:hover { border-color: var(--edge-strong); }
/* The filter row is chrome sitting under the header, not a second header. Say so. */
.grid-host .ag-header-row-column-filter { background: var(--anodised); }

/* Dashboard tiles are draggable. The handle stays quiet until the card is hovered — the
   number is what the reader came for, not the furniture. */
.rb-tile { cursor: default; }
.rb-drag {
  color: var(--ink-4); font-size: .85rem; line-height: 1; cursor: grab;
  opacity: 0; transition: opacity 120ms ease-out; user-select: none;
}
.rb-tile:hover .rb-drag, .rb-tile:focus-within .rb-drag { opacity: 1; }
.rb-tile.rb-dragging { opacity: .45; }
.rb-tile.rb-dragging .card { outline: 2px dashed var(--plotter); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .rb-drag { transition: none; } }

/* =====================================================================================
   One control scale — Tabler's side.

   Reported 2026-08-22: the type and field sizes felt off. They were. On one form the
   controls measured 26, 32, 38, 40 and 41px tall, with 12.8px, 13.44px and 13.6px type —
   not a scale, just what .8rem, .84rem and .85rem round to. Tabler's own defaults were
   half of that disagreement, so they are pinned to the same ladder as everything else
   (style.css --ctl-*). Nothing next to anything else is a different height by accident now.
   ===================================================================================== */

.form-control, .form-select, .input-icon > .form-control {
  height: var(--ctl-md);
  min-height: var(--ctl-md);
  padding: 0 var(--ctl-pad);
  font-size: var(--ctl-font);
  line-height: 1.35;
  border-radius: var(--radius-s);
  border-color: var(--edge);
  background-color: var(--inset);
  color: var(--chart-ink);
  transition: border-color 120ms ease-out, box-shadow 120ms ease-out, background-color 120ms ease-out;
}
textarea.form-control { height: auto; min-height: calc(var(--ctl-md) * 2); padding: .5rem var(--ctl-pad); line-height: 1.5; }
.form-control:hover:not(:focus), .form-select:hover:not(:focus) { border-color: var(--edge-strong); }
.form-control:focus, .form-select:focus {
  border-color: var(--plotter);
  background-color: var(--canvas);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--plotter) 18%, transparent);
}
/* Specificity, not intent: `.input-icon > .form-control` (0,2,0) outranked `.form-control-sm`
   (0,1,0), so every "small" field in a grid toolbar came out full height. Name both. */
.form-control-sm, .form-select-sm,
.input-icon > .form-control-sm, .input-icon > .form-select-sm {
  height: var(--ctl-sm); min-height: var(--ctl-sm);
  font-size: var(--t-ui); padding: 0 .5rem; border-radius: 6px;
}

.btn {
  height: var(--ctl-md);
  padding: 0 calc(var(--ctl-pad) * 1.6);
  font-size: var(--t-ui);
  font-weight: 500;
  line-height: 1;
  border-radius: var(--radius-s);
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
  /* Apple §1: acknowledge the press on pointer-down, not on release. */
  transition: transform 110ms cubic-bezier(0.23, 1, 0.32, 1), background-color 110ms ease-out;
}
.btn:active { transform: scale(.97); }
.btn-sm { height: var(--ctl-sm); padding: 0 .625rem; font-size: var(--t-small); border-radius: 6px; }
.btn:focus-visible { outline: 2px solid var(--plotter); outline-offset: 2px; box-shadow: none; }

/* Type: the same ladder the rest of the product uses, so Tabler's headings stop being a
   second opinion. Tracking is size-specific — one letter-spacing is wrong somewhere. */
.page-title  { font-size: var(--t-h2);   letter-spacing: var(--track-tight); line-height: 1.2; }
.card-title  { font-size: var(--t-h3);   letter-spacing: -.01em; }
.page-pretitle { font-size: var(--t-micro); letter-spacing: var(--track-caps); }
.small, small { font-size: var(--t-small); }
.h1 { font-size: var(--t-h1); letter-spacing: var(--track-tight); line-height: 1.05; }
.table { font-size: var(--t-small); }
.table th { font-size: var(--t-micro); letter-spacing: var(--track-caps); text-transform: uppercase; }

/* The grid inherits the same sizes rather than carrying its own. */
.grid-host { --ag-font-size: 13px; --ag-header-font-size: 11px; }

/* A finger is not a mouse: everything reaches the 44px target, and control text goes to
   16px so iOS does not zoom the page on focus. */
@media (pointer: coarse) {
  .form-control, .form-select, .btn, .form-control-sm, .form-select-sm, .btn-sm {
    min-height: var(--ctl-lg); height: auto; font-size: 1rem;
  }
  .form-control, .form-select { padding: .5rem var(--ctl-pad); }
  .navbar-vertical .nav-link { padding-top: .7rem; padding-bottom: .7rem; }
}

/* Apple §14. Reduced motion is not "no feedback" — it is the same feedback without the
   vestibular part. Reduced transparency makes the material solid rather than removing it.
   More contrast fills the surfaces and gives every edge a defined line. */
@media (prefers-reduced-motion: reduce) {
  .btn, button, .button, .form-control, .form-select, .rb-drag { transition: none !important; }
  .btn:active, button:active, .button:active { transform: none; }
}
@media (prefers-reduced-transparency: reduce) {
  .navbar, .page-header { backdrop-filter: none !important; background: var(--canvas) !important; }
}
@media (prefers-contrast: more) {
  .card, .form-control, .form-select, .btn { border: 1px solid var(--edge-strong) !important; }
  .rb-pill, .rb-delta { border-width: 1.5px; }
  .grid-host .ag-row { border-bottom-style: solid; }
}

/* The trend line beside a movement figure. Thin mark, no axis, no grid — a sparkline is read
   as a shape, and anything else drawn around it competes with the number it belongs to.
   The line stays muted; only the endpoint carries the tone, matching the pill next to it.
   Colouring the whole line would make an activity count look like a status. */
.rb-spark { display: block; overflow: visible; }
.rb-spark-line {
  fill: none;
  stroke: var(--ink-4);
  stroke-width: 1.5;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.rb-spark-dot { fill: var(--ink-3); }
.rb-spark-dot.rb-delta-up   { fill: var(--led-good); }
.rb-spark-dot.rb-delta-down { fill: var(--estop); }
.rb-spark-dot.rb-delta-flat { fill: var(--ink-3); }
@media print { .rb-spark-line { stroke: #666; } }

/* =====================================================================================
   The frame — 2026-08-22.

   The owner asked why the reference feels finished and this did not. The tokens were never
   the problem; the composition was. Three things the reference does that this did not:

   1. THE APP SITS IN SOMETHING. Their whole product floats on a ground with real air around
      it and a generous corner radius — it reads as an object you are holding. Ours ran
      edge-to-edge from browser chrome to browser chrome, which is what a document does, not
      what an application does. This is the single largest "unfinished" signal and the
      cheapest to fix.
   2. NOTHING IS CRAMPED AGAINST THE EDGE. Content keeps its distance from the frame.
   3. THE FRAME IS ONLY FOR ROOM TO SPARE. Below a large viewport it goes full-bleed, because
      on an iPad in an engine room every pixel is content and a decorative margin is theft.
   ===================================================================================== */

@media (min-width: 1280px) {
  /* The frame is the scroll container, not the document.

     First attempt kept Tabler's fixed rail and let the page scroll behind it — reported the
     same day: "de side bar en achtergrond zitten elkaar dwars bij scrollen". They did, and
     they always would have: a position:fixed rail lives in the viewport while the frame lives
     in the document, so the frame's rounded top edge scrolled up past a rail that stayed put,
     and the two corners came apart. Two elements pretending to be one object have to share a
     coordinate system.

     So above this width the shell behaves like an application window: the body does not
     scroll, the frame fills what is left of the viewport, the rail is an ordinary flex child
     of it, and only the content column scrolls. Below it, everything reverts to Tabler's
     default — a fixed rail and a scrolling document, which is right on a tablet. */
  html { height: 100%; }
  body {
    height: 100%;
    overflow: hidden;
    background: var(--plane);
    padding: clamp(14px, 1.6vw, 26px);
  }
  .page {
    height: 100%;
    display: flex;
    /* Tabler already sets .page to flex-direction: column, so declaring display:flex alone
       inherited that and stacked the rail on top of the content instead of beside it. */
    flex-direction: row;
    overflow: hidden;                 /* clips the rail into the frame's own corners */
    background: var(--canvas);
    border-radius: 20px;
    box-shadow: 0 0 0 1px var(--edge-soft), 0 12px 32px -16px rgba(20, 20, 22, .18);
  }

  /* The rail joins the frame instead of floating over it. Its own radius goes: the frame
     clips it, which is also what stops the active item's accent edge from poking out past
     the rounded corner. */
  .navbar-vertical.navbar-expand-lg {
    position: relative;
    inset: auto;
    flex: 0 0 15rem;
    height: auto;
    align-self: stretch;
    border-radius: 0;
    border-right: 1px solid var(--edge-soft);
    overflow-y: auto;
    overscroll-behavior: contain;     /* a long menu must not scroll the page behind it */
  }

  /* Tabler reserves 15rem for the fixed rail; there is nothing to reserve any more. */
  .navbar-vertical.navbar-expand-lg ~ .page-wrapper {
    margin-left: 0;
    flex: 1 1 auto;
    min-width: 0;                     /* or a wide grid refuses to shrink and overflows */
    min-height: 0;                    /* or overflow-y never engages on a flex child */
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}
@media (min-width: 1280px) and (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .page { box-shadow: 0 0 0 1px var(--edge); }
}

/* Paper has no viewport. Undo the whole shell so the content flows as one document. */
@media print {
  html, body { height: auto; overflow: visible; padding: 0; }
  .page { display: block; height: auto; overflow: visible; box-shadow: none; border-radius: 0; }
  .page-wrapper { overflow: visible !important; }
}

/* =====================================================================================
   The page header.

   Was: a title, a primary button and a run of plain text on one line, three unrelated
   things with no hierarchy between them. The reference separates them — title on the left,
   controls on the right, and the account is a control rather than a sentence.
   ===================================================================================== */

.page-header { padding-top: 1.5rem; padding-bottom: .75rem; }
.page-title { margin-bottom: 0; }

/* A chip: the secondary control shape. Outlined, fully rounded, quiet until hovered — the
   reference uses exactly two of these in its header and nothing else. */
.rb-chip {
  display: inline-flex; align-items: center; gap: .4rem;
  height: var(--ctl-sm); padding: 0 .75rem;
  border: 1px solid var(--edge); border-radius: 999px;
  background: var(--canvas); color: var(--ink-2);
  font-size: var(--t-small); font-weight: 500; text-decoration: none;
  white-space: nowrap;
  transition: border-color 120ms ease-out, background 120ms ease-out, transform 110ms cubic-bezier(.23,1,.32,1);
}
.rb-chip:hover { border-color: var(--edge-strong); color: var(--chart-ink); background: var(--anodised); }
.rb-chip:active { transform: scale(.97); }
.rb-chip svg { width: 15px; height: 15px; opacity: .75; }
.rb-chip-quiet { border-color: transparent; background: transparent; }

/* =====================================================================================
   The in-column filter, again.

   Seven columns each saying the word "filter…" is seven repetitions of one idea. The
   reference never repeats a label down a row; it uses a glyph once and lets the shape carry
   the meaning. The hint is now a filter mark inside the field, at low contrast, and the word
   is gone.
   ===================================================================================== */

/* AG Grid injects `.ag-input-field-input { padding: 0 !important }` at runtime, after this
   stylesheet, so the room for the mark has to be claimed the same way. The rest is ordinary
   cascade. */
/* Specificity, again, and this one is subtle enough to be worth writing down. The rule above
   that resets the field lists two selectors — `… .ag-input-field-input` (0,3,0) and
   `… input` (0,2,1) — and the element matches BOTH. A selector list is scored per selector,
   so the 0,3,0 branch carrying `background: transparent` beat a later rule written as
   `… input` and the mark never drew, in every state, while looking perfectly correct in the
   file. Match the heavier branch. */
.grid-host .ag-floating-filter .ag-input-field-input {
  padding-left: 24px !important;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236a6a6f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' opacity='.42'%3E%3Cpath d='M4 6h16M7 12h10M10 18h4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: 6px center;
  background-size: 13px 13px;
}
/* The mark stays put the way a search glyph does — it was briefly wired to
   :not(:placeholder-shown), which never matches an empty placeholder attribute and so hid
   the glyph in every state. A mark that only exists while the field is empty is also just
   a placeholder wearing a costume. */

/* =====================================================================================
   Motion between pages.

   Every navigation here is a full document load, which on a server-rendered app means a
   white flash and a hard cut — the thing that most makes a working application feel like a
   sequence of reports. One line of CSS gives the browser permission to cross-fade instead.
   Where it is unsupported nothing changes; where reduced motion is asked for it is off.
   ===================================================================================== */

@view-transition { navigation: auto; }
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}
::view-transition-old(root), ::view-transition-new(root) { animation-duration: 140ms; }

/* =====================================================================================
   Fleet rollout.

   A vessel code is a thing you scan for, not a sentence you read — so it gets a chip, not a
   link. Rendered as bare links they inherited the accent, and after the palette moved to
   green the "not seen" column became an undivided wall of it (reported 2026-08-22). Chips
   give each hull its own edge, so a run of six reads as six.
   ===================================================================================== */

.rb-nowrap { white-space: nowrap; }

.rb-rollout td { vertical-align: top; padding-top: .7rem; padding-bottom: .7rem; }
.rb-rollout .rb-rule-title { display: block; margin-top: .3rem; color: var(--chart-ink); }
.rb-tape-link { text-decoration: none; border: 0; }

.rb-vessels { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.rb-vessels-seen { margin-top: .45rem; }
.rb-vessels-label {
  font-size: var(--t-micro); text-transform: uppercase; letter-spacing: var(--track-caps);
  color: var(--ink-4); margin-right: .15rem;
}
.rb-vessel {
  display: inline-flex; align-items: center;
  font-size: 11.5px; font-variant-numeric: tabular-nums; letter-spacing: -.004em;
  padding: 2px 7px; border-radius: 6px; white-space: nowrap; text-decoration: none;
  border: 1px solid var(--edge); background: var(--anodised); color: var(--ink-2);
  transition: border-color 110ms ease-out, color 110ms ease-out;
}
.rb-vessel:hover { border-color: var(--edge-strong); color: var(--chart-ink); }
/* Proven is the one state on this screen that earns colour, and it is the reserved good. */
.rb-vessel-seen {
  background: var(--led-good-bg); border-color: color-mix(in srgb, var(--led-good) 22%, transparent);
  color: var(--led-good);
}

/* Coverage: the number is what you quote, the bar is what you see without reading. */
.rb-cover {
  width: 88px; height: 5px; border-radius: 3px;
  background: var(--anodised-deep); overflow: hidden; margin-bottom: .3rem;
}
.rb-cover-fill { display: block; height: 100%; background: var(--led-good); border-radius: 3px; }
.rb-cover-num { font-size: var(--t-small); font-variant-numeric: tabular-nums; color: var(--ink-3); }

@media print {
  .rb-vessel { background: none; border-color: #999; color: #000; }
  .rb-cover  { border: 1px solid #999; }
}

/* Kinds that only appear on a visit log or a rule page, folded into the one pill component
   so nothing renders its own private version of "this is a status". */
.rb-pill-observation, .rb-pill-organisation { background: var(--anodised-deep); color: var(--ink-2); border-color: var(--edge); }
.rb-pill-action  { background: var(--annunciator-bg); color: var(--annunciator);
                   border-color: color-mix(in srgb, var(--annunciator) 22%, transparent); }
.rb-pill-finding { background: var(--estop-bg); color: var(--estop);
                   border-color: color-mix(in srgb, var(--estop) 22%, transparent); }

/* The comparison. Two sides, aligned row by row — a real two-table layout would put the
   same clause on different lines and make the reader do the aligning. */
.rb-cmp td { vertical-align: top; }
.rb-cmp-changed    td { background: color-mix(in srgb, var(--annunciator) 5%, transparent); }
.rb-cmp-only-left  td { background: color-mix(in srgb, var(--estop) 5%, transparent); }
.rb-cmp-only-right td { background: color-mix(in srgb, var(--plotter) 5%, transparent); }
.rb-clause-head { font-weight: 600; font-size: var(--t-small); margin-bottom: .15rem; }
.rb-clause-body { font-size: var(--t-small); color: var(--ink-2); line-height: 1.55; }
.rb-clause-body b { font-weight: 600; color: var(--ink-3); font-size: var(--t-micro);
                    text-transform: uppercase; letter-spacing: .05em; }
/* The changed field itself, not the whole row — the row says something changed, this says what. */
.rb-diff {
  background: color-mix(in srgb, var(--annunciator) 22%, transparent);
  border-radius: 3px; padding: 0 3px; font-weight: 600; color: var(--chart-ink);
}
@media print { .rb-diff { background: none; text-decoration: underline; } }

/* The evidence under a difference: the actual config lines, left and right. */
.rb-cmp-raw td { border-top: 0; padding-top: 0; }
.rb-raw-toggle {
  cursor: pointer; font-size: var(--t-small); color: var(--plotter);
  list-style: none; display: inline-flex; align-items: center; gap: .35rem; padding: .15rem 0;
}
.rb-raw-toggle::-webkit-details-marker { display: none; }
.rb-raw-toggle::before { content: "▸"; font-size: .7em; transition: transform 110ms ease-out; }
details[open] > .rb-raw-toggle::before { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .rb-raw-toggle::before { transition: none; } }

.rb-raw { margin-top: .5rem; border: 1px solid var(--edge); border-radius: var(--radius-s); overflow: hidden; }
.rb-raw-head {
  display: grid; grid-template-columns: 1fr 1fr; gap: 1px;
  background: var(--anodised); font-size: var(--t-micro); font-weight: 600;
  text-transform: uppercase; letter-spacing: var(--track-caps); color: var(--ink-3);
}
.rb-raw-head span { padding: .35rem .6rem; }
.rb-raw-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--edge-soft); }
.rb-raw-row code {
  display: block; padding: .2rem .6rem; background: var(--canvas);
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace; font-size: 11.5px;
  white-space: pre-wrap; word-break: break-word; min-height: 1.5em;
}
.rb-raw-changed code { background: color-mix(in srgb, var(--annunciator) 12%, var(--canvas)); }
.rb-raw-removed code:first-child { background: color-mix(in srgb, var(--estop) 12%, var(--canvas)); }
.rb-raw-added   code:last-child  { background: color-mix(in srgb, var(--led-good) 12%, var(--canvas)); }
.rb-clause-off td { opacity: .55; }

/* A clause list outside a comparison: one column, so the sentence gets the full width. */
.rb-raw-single .rb-raw-row { grid-template-columns: 1fr; }
.rb-cmp tbody tr:not(.rb-cmp-raw) td { padding-bottom: .2rem; }

/* The vendor mark: the manufacturer's own logo, small. Constrained by HEIGHT and left to
   find its own width, because these files are anything from 250x31 to 250x132 — forcing a
   box would squash half of them. It sits in its own column and never beside a status pill,
   so a brand colour never competes with a colour that means something (see vendors.php). */
/* Bounded on BOTH axes rather than fixed on one: these files run from 250x31 (a flat
   wordmark) to 250x132 (a mark stacked over a wordmark), and a single height makes the tall
   ones illegible while a single width squashes the wide ones. object-fit does the rest. */
.rb-mark {
  max-height: 20px; max-width: 4.75rem; height: auto; width: auto;
  object-fit: contain; object-position: left center;
  flex: none; vertical-align: -4px;
}
.rb-mark-text {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 2.1rem; height: 16px; padding: 0 .3rem;
  background: var(--anodised-deep); color: var(--ink-2); border: 1px solid var(--edge);
  border-radius: 3px; font-size: 9px; font-weight: 700; letter-spacing: .04em;
}
.rb-mark-wrap { display: inline-flex; align-items: center; gap: .45rem; min-width: 0; }
/* When only the mark is wanted, the name is still there for a screen reader. */
.rb-mark-name {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
/* A monochrome print of six logos is six grey smudges. On paper the name does the work. */
@media print {
  .rb-mark { display: none !important; }
  .rb-mark-name { position: static; width: auto; height: auto; clip-path: none; }
}

/* A row with a stacked logo in it needs the height, or the mark is cropped by the cell. */
.grid-host { --ag-row-height: 40px; }
.grid-host .ag-cell { display: flex; align-items: center; }

/* Whose tool this is. The wordmark sits above the product name rather than replacing it —
   a logo on its own leaves a new engineer wondering which of the ONE-XP screens is open. */
.rb-brand { display: block; width: 108px; height: auto; margin-bottom: .15rem; }
.rb-brand-product {
  display: block; font-size: .95rem; font-weight: 620; letter-spacing: -.015em;
  color: var(--chart-ink); line-height: 1.1;
}
.navbar-vertical .navbar-brand { padding-top: .9rem; padding-bottom: .6rem; }
/* On paper the mark stays: a printed fleet board should say who produced it. */
@media print { .rb-brand { width: 90px; } }

/* A manufacturer's logo is drawn for a light background, and six of them cannot be hand-tuned
   for a dark one. On dark, each sits on a small light plate — the same thing a printed report
   does — so the mark keeps its own colours and stays legible instead of vanishing. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .rb-mark {
    background: #f4f4f5; border-radius: 3px; padding: 2px 4px;
    box-sizing: content-box;
  }
}
:root[data-theme="dark"] .rb-mark {
  background: #f4f4f5; border-radius: 3px; padding: 2px 4px; box-sizing: content-box;
}
/* Our own mark ships a dark variant instead, so it must not get the plate. */
.rb-brand { background: none !important; padding: 0 !important; }

/* The checklist. A step is a row with room for a sentence and a photograph, not a tick box. */
.rb-steps td { vertical-align: top; padding-top: .75rem; padding-bottom: .75rem; }
.rb-step-done td      { background: color-mix(in srgb, var(--led-good) 4%, transparent); }
.rb-step-partial td   { background: color-mix(in srgb, var(--annunciator) 6%, transparent); }
.rb-step-open td      { background: transparent; }
.rb-step-note {
  margin-top: .4rem; padding-left: .7rem; border-left: 2px solid var(--edge);
  font-size: var(--t-small); color: var(--ink-2);
}
.rb-photos { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: .6rem; }
.rb-photos figure { margin: 0; max-width: 11rem; }
.rb-photos img {
  display: block; width: 100%; height: auto; border-radius: var(--radius-s);
  border: 1px solid var(--edge); background: var(--anodised);
}
.rb-photos figcaption { font-size: var(--t-micro); color: var(--ink-3); margin-top: .2rem; }

/* The brand mark, swapped by theme. Two images rather than <picture media>, because a media
   query follows the OS and would ignore the manual toggle. */
.rb-brand-dark { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .rb-brand-light { display: none; }
  :root:not([data-theme="light"]) .rb-brand-dark  { display: block; }
}
:root[data-theme="dark"] .rb-brand-light { display: none; }
:root[data-theme="dark"] .rb-brand-dark  { display: block; }
:root[data-theme="light"] .rb-brand-light { display: block; }
:root[data-theme="light"] .rb-brand-dark  { display: none; }
.rb-theme { cursor: pointer; }

/* Dragging a checklist step. The handle appears on hover, like the dashboard tiles. */
.rb-drag-step { color: var(--ink-4); cursor: grab; user-select: none; margin-right: .35rem;
                opacity: 0; transition: opacity 120ms ease-out; }
.rb-step:hover .rb-drag-step, .rb-step:focus-within .rb-drag-step { opacity: 1; }
.rb-step-dragging { opacity: .5; }
.rb-step-dragging td:first-child { box-shadow: inset 2px 0 0 var(--plotter); }
@media (prefers-reduced-motion: reduce) { .rb-drag-step { transition: none; } }

/* A long config scrolls inside its own card instead of pushing the page down. The height is
   generous enough to read a dozen rules without scrolling at all, and capped so the rest of
   the project page stays reachable. */
.rb-clause-scroll { max-height: 30rem; overflow-y: auto; overscroll-behavior: contain; }
@media print { .rb-clause-scroll { max-height: none; overflow: visible; } }

/* =====================================================================================
   The VLAN matrix. A heatmap, not a table — read by shape and colour, with the answer to
   "which rule" one click behind every cell.
   ===================================================================================== */

.rb-matrix { font-size: var(--t-small); border-collapse: separate; border-spacing: 2px; }
.rb-matrix th {
  font-size: var(--t-micro); text-transform: uppercase; letter-spacing: var(--track-caps);
  color: var(--ink-3); font-weight: 600; white-space: nowrap; padding: .4rem .5rem;
}
.rb-matrix thead th { text-align: center; }
.rb-matrix tbody th { text-align: right; background: var(--anodised); border-radius: 4px; }
.rb-matrix th a { color: inherit; text-decoration: none; border-bottom: 1px dotted var(--edge-strong); }
.rb-matrix th a:hover { color: var(--chart-ink); }
.rb-matrix-corner { text-align: left !important; color: var(--ink-4) !important; }
.rb-matrix td {
  text-align: center; padding: .35rem .4rem; border-radius: 4px; white-space: nowrap;
  min-width: 5.5rem;
}
.rb-matrix td a { text-decoration: none; color: inherit; display: block; }

/* The four states carry the reserved scale: an allow between two zones that should not talk
   is the finding, and it has to read as one from two metres away. */
.rb-cell-allow   { background: var(--led-good-bg);    color: var(--led-good); }
.rb-cell-trust   { background: var(--annunciator-bg); color: var(--annunciator); }
.rb-cell-deny    { background: var(--estop-bg);       color: var(--estop); }
.rb-cell-monitor { background: var(--anodised-deep);  color: var(--ink-2); }
.rb-cell-other   { background: var(--anodised);       color: var(--ink-2); }
.rb-cell-none    { background: transparent; }
.rb-cell-empty   { color: var(--ink-4); }
.rb-cell-act     { font-weight: 600; }
/* A pair reached only by a wildcard is a different fact from one somebody wrote a rule for. */
.rb-cell-wild    { opacity: .8; font-weight: 700; margin-left: .15rem; }
.rb-cell-behind  { font-size: var(--t-micro); opacity: .75; margin-left: .2rem; }

@media print {
  .rb-matrix td { border: 1px solid #999; }
  .rb-cell-allow::after { content: " (allow)"; }
  .rb-cell-deny::after  { content: " (deny)"; }
}

/* Two scratch sides on the comparison screen (2026-08-22). The slots are drawn as a pair of
   quiet inset panels rather than two loose stacks of fields: the screen's whole claim is that
   these are the two halves of one comparison, and unbounded fields read as a list of unrelated
   controls. Inset, because they receive content — same reasoning as the inputs elsewhere. */
.rb-scratch-slot {
  border: 1px solid var(--edge);
  border-radius: var(--radius-m);
  background: var(--inset);
  padding: .75rem;
  height: 100%;
}
.rb-scratch-slot textarea { resize: vertical; }

/* ============================================ the VLAN matrix, inline on the project page

   Dense on purpose. Fifty-eight VLANs is 3,364 pairs, and the whole reason to draw a matrix
   instead of a list is that the eye finds the odd cell without reading. So the cells are small
   and carry only colour; every word lives in the detail pane that opens on click.

   Colour is never the only signal — a diverging cell also carries a mark, because roughly one
   man in twelve cannot separate this palette by hue and a security finding is the last thing to
   hide behind a colour. */
.rb-vm-head { display: flex; flex-wrap: wrap; gap: 1rem; align-items: baseline; margin-bottom: .5rem; }
.rb-vm-stat { color: var(--ink-3); font-size: var(--t-small); }
.rb-vm-stat b { color: var(--ink); font-variant-numeric: tabular-nums; }
.rb-vm-stat-bad b { color: var(--estop); }

.rb-vm-scroll {
  overflow: auto;
  /* A floor as well as a ceiling. A bare 62vh collapses to nothing wherever the viewport height
     resolves to zero — an embedded pane, a print context, a headless capture — and the matrix
     silently disappears while the page around it looks fine. */
  height: max(26rem, 62vh);
  max-height: none;
  /* Drag the bottom edge. The browser's own handle, which means it works with the keyboard and
     the touch bar and everything else the platform already solved — and a matrix is exactly the
     thing where one reader wants six rows and the next wants sixty. */
  resize: vertical;
  border: 1px solid var(--edge);
  border-radius: var(--radius-m);
  background: var(--inset);
}
.rb-vm { border-collapse: separate; border-spacing: 1px; font-size: 10px; }
.rb-vm th {
  position: sticky; background: var(--plane); z-index: 2;
  font-weight: 500; color: var(--ink-3); white-space: nowrap;
}
.rb-vm thead th { top: 0; padding: 3px 2px; text-align: center; min-width: 15px; }
.rb-vm tbody th {
  left: 0; text-align: right; padding: 0 .5rem 0 .35rem;
  font-size: var(--t-micro); max-width: 190px; overflow: hidden; text-overflow: ellipsis;
}
.rb-vm tbody th a { color: inherit; text-decoration: none; }
.rb-vm tbody th a:hover { color: var(--chart-ink); text-decoration: underline; }
.rb-vm-id { color: var(--ink-4); font-variant-numeric: tabular-nums; }
.rb-vm-corner { left: 0; top: 0; z-index: 3; text-align: left !important; color: var(--ink-4) !important; }

.rb-vm td {
  width: 15px; height: 15px; padding: 0; border-radius: 2px;
  cursor: pointer; position: relative;
}
.rb-vm td:focus-visible { outline: 2px solid var(--action); outline-offset: 1px; }
.rb-vm-behind {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-size: 8px; color: var(--ink-4);
}

/* The scale. Allowed and denied are quiet — most of the matrix is one or the other and it must
   read as texture, not as alarm. The three that mean something stand out. */
/* THE SCALE.

   Two states carry most of the grid — reachable and blocked — and they must read as texture, not
   as alarm: a matrix where three thousand cells shout has no signal left for the forty that
   should. So the field is low-chroma and separated by LIGHTNESS as much as by hue, which is what
   keeps it legible for the roughly one man in twelve who cannot split this palette by hue alone.

   The findings then take saturated colour, which is a scarce resource spent here and nowhere
   else on the page. */
.rb-vm td { transition: transform 90ms ease-out, box-shadow 90ms ease-out; }

.rb-vm-allow     { background: color-mix(in oklab, var(--led-good) 42%, var(--plane)); }
.rb-vm-deny      { background: color-mix(in oklab, var(--ink-4) 16%, var(--plane)); }
.rb-vm-exception { background: color-mix(in oklab, var(--led-good) 18%, var(--plane));
                   box-shadow: inset 0 -2px 0 color-mix(in oklab, var(--st-good) 60%, transparent); }
.rb-vm-wider     { background: var(--estop);
                   box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--estop) 55%, #000); }
.rb-vm-narrower  { background: var(--annunciator);
                   box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--annunciator) 55%, #000); }

/* Alive under the pointer. A cell lifts slightly and both its headers light up, so the eye can
   follow a row across fifty-eight columns without losing the line — the single thing that makes
   a dense grid usable rather than impressive. */
.rb-vm td:hover { transform: scale(1.9); z-index: 4; position: relative;
                  box-shadow: 0 0 0 1px var(--ink), var(--lift-sm); border-radius: 3px; }
.rb-vm th[aria-current] { color: var(--ink); background: var(--raised); }
.rb-vm td[aria-current] { box-shadow: 0 0 0 2px var(--action); z-index: 5; }

/* The clause that makes everything below it dead. Nothing else on this screen may look like
   this — it is the one finding that changes what every other number means. */
.rb-vm-shadow {
  margin: .25rem 0 .75rem;
  border: 1px solid var(--estop);
  border-left: 4px solid var(--estop);
  border-radius: var(--radius-m);
  background: color-mix(in oklab, var(--estop) 7%, var(--plane));
  padding: .7rem .9rem;
  font-size: var(--t-small);
}
.rb-vm-shadow b { color: var(--estop); }
.rb-vm-shadow pre {
  margin: .5rem 0 0; padding: .5rem .65rem;
  /* Its own surface, not the banner's tinted one — code on a red wash is unreadable, which is
     what the first version shipped. */
  background: var(--canvas); color: var(--ink);
  border-radius: var(--radius-s); overflow-x: auto; font-size: var(--t-micro);
  border: 1px solid var(--edge);
  max-height: 11rem;
}
.rb-vm-none      { background: transparent; cursor: default; }

.rb-vm-legend {
  display: flex; flex-wrap: wrap; gap: .35rem 1rem; align-items: center;
  margin-top: .5rem; font-size: var(--t-micro); color: var(--ink-3);
}
.rb-vm-legend i { display: inline-block; width: 11px; height: 11px; border-radius: 2px;
                  vertical-align: -1px; margin-right: .3rem; }
.rb-vm-legend i.rb-vm-none { border: 1px dashed var(--edge-strong); }
.rb-vm-legend-note { color: var(--ink-4); }

.rb-vm-detail {
  margin-top: .75rem; border: 1px solid var(--edge); border-radius: var(--radius-m);
  background: var(--plane); padding: .75rem 1rem;
}
.rb-vm-detail h4 { margin: 0 0 .35rem; font-size: var(--t-body); }
.rb-vm-detail pre {
  margin: .5rem 0 0; padding: .5rem .65rem; background: var(--inset);
  border-radius: var(--radius-s); overflow-x: auto; font-size: var(--t-micro);
}
.rb-vm-detail .rb-vm-verdict { font-size: var(--t-small); color: var(--ink-3); }

@media print {
  .rb-vm-scroll { max-height: none; overflow: visible; }
  .rb-vm td { border: 1px solid #999; }
}

.rb-vm-proto { display: inline-flex; align-items: center; gap: .4rem;
               font-size: var(--t-small); color: var(--ink-3); }
.rb-vm-proto select { width: auto; min-width: 12rem; }
.rb-vm-recount { color: var(--ink-3); font-size: var(--t-small); }

/* The VLAN plan against the template. A list, not a grid: there are dozens of differences at
   most, each needs a sentence, and forcing them into the matrix would hide the words. */
.rb-tpl { width: 100%; border-collapse: collapse; font-size: var(--t-small); }
.rb-tpl thead th {
  position: sticky; top: 0; background: var(--plane); z-index: 1;
  text-align: left; font-weight: 500; color: var(--ink-3);
  padding: .45rem .6rem; border-bottom: 1px solid var(--edge);
}
.rb-tpl td { padding: .45rem .6rem; border-bottom: 1px solid var(--edge-soft); vertical-align: top; }
.rb-tpl tbody tr:hover { background: var(--raised); }
.rb-tpl td:first-child { font-variant-numeric: tabular-nums; color: var(--ink-3); width: 4rem; }
.rb-vm-absent { color: var(--ink-4); }
.rb-vm-tplnote { color: var(--ink-3); font-size: var(--t-micro); margin-top: .2rem; max-width: 46ch; }

/* ===================================== foldable, reorderable sections (project page)

   Built on <details>, which is the platform's own fold: keyboard, screen readers and
   find-in-page all understand it, and print can force it open without a line of script. */
.rb-sec { border-top: 1px solid var(--edge); margin-top: 1.25rem; }
.rb-sec:first-of-type { border-top: 0; margin-top: .5rem; }
.rb-sec-head {
  display: flex; align-items: center; gap: .5rem;
  padding: .6rem 0 .1rem; cursor: pointer; list-style: none;
  color: var(--ink);
}
.rb-sec-head::-webkit-details-marker { display: none; }
.rb-sec-head::before {
  content: "▸"; font-size: .7em; color: var(--ink-4);
  transition: transform 120ms ease-out;
}
.rb-sec[open] > .rb-sec-head::before { transform: rotate(90deg); }
.rb-sec-head:hover::before { color: var(--ink-3); }
.rb-sec-title { font-size: var(--t-h2, 1.05rem); font-weight: 600; letter-spacing: -0.01em; }
/* A folded section is still a section: the heading stays, so nothing is ever invisible. */
.rb-sec:not([open]) > .rb-sec-head { opacity: .75; }
.rb-sec-body { padding-top: .35rem; }

/* The blocks are their own handles. A grip appears on the heading when the pointer is over it —
   present enough to invite the drag, absent enough not to clutter eight headings at rest. */
/* A corner well, drawn with dots rather than labelled with a word: eight headings each saying
   "drag" is clutter, and a dotted grip is the one affordance every desktop reader already knows.
   Faintly present at rest so it can be discovered, solid under the pointer so it invites. */
.rb-sec-grip {
  cursor: grab; color: transparent; width: 12px; height: 16px; flex: 0 0 auto;
  background-image: radial-gradient(currentColor 1px, transparent 1.2px);
  background-size: 5px 5px; background-position: 0 2px; background-repeat: repeat;
  color: var(--edge-strong);
  transition: color 120ms ease-out;
}
.rb-sec-head:hover .rb-sec-grip,
.rb-sec-head:hover .rb-fb-toggle { color: var(--ink-4); opacity: 1; }
.rb-sec-grip:active { cursor: grabbing; }
.rb-sec-dragging { opacity: .4; }
.rb-sec-over { box-shadow: 0 -2px 0 var(--action); }
.rb-sec-hint { font-size: var(--t-micro); color: var(--ink-4); margin: .25rem 0 0; text-align: right; }
.rb-linkish {
  background: none; border: 0; padding: 0; font: inherit; color: var(--ink-3);
  text-decoration: underline; cursor: pointer;
}
.rb-linkish:hover { color: var(--ink); }

@media print {
  .rb-sec { border-top: 1px solid #ccc; }
  .rb-sec:not([open]) > .rb-sec-body { display: block !important; }
  .rb-sec-head::before { display: none; }
}

.rb-vm-caveat {
  margin: .1rem 0 .4rem; font-size: var(--t-small); color: var(--ink-3);
  border-left: 3px solid var(--estop); padding-left: .55rem;
}
.rb-vm-caveat b { color: var(--ink); }

.rb-vm-exceptions { margin: .25rem 0 .35rem; padding-left: 1.1rem; }
.rb-vm-exceptions li { font-size: var(--t-small); color: var(--ink-2, var(--ink)); }
.rb-vm-exceptions .rb-vm-more { color: var(--ink-4); }
.rb-vm-diverge { color: var(--estop); }

/* The remark control on a block's heading. Quiet until wanted: eight headings each shouting for
   feedback would be worse than not asking at all. */
.rb-sec-actions { margin-left: auto; display: inline-flex; align-items: center; }
.rb-fb { position: relative; }
.rb-fb-toggle {
  list-style: none; cursor: pointer; display: inline-flex; align-items: center;
  padding: .2rem .35rem; border-radius: var(--radius-s); color: var(--ink-4);
  opacity: 0; transition: opacity 120ms ease-out;
}
.rb-fb-toggle::-webkit-details-marker { display: none; }
.rb-sec-head:hover .rb-fb-toggle,
.rb-fb[open] .rb-fb-toggle { opacity: 1; }
.rb-fb-toggle:hover { background: var(--raised); color: var(--ink); }
.rb-fb-body {
  position: absolute; right: 0; top: 100%; z-index: 30; width: min(26rem, 92vw);
  background: var(--plane); border: 1px solid var(--edge-strong);
  border-radius: var(--radius-m); box-shadow: var(--lift); padding: .75rem .85rem;
  text-align: left; font-weight: 400;
}
.rb-fb-verdicts { display: flex; flex-wrap: wrap; gap: .75rem; margin-bottom: .5rem; }
.rb-fb-verdicts label { font-size: var(--t-small); display: inline-flex; align-items: center; gap: .3rem; }
.rb-fb-reply {
  border-left: 2px solid var(--edge-strong); padding: .25rem 0 .25rem .6rem;
  margin: .35rem 0 0 .5rem; font-size: var(--t-small);
}

/* One click, always visible. The version before this hid a chat icon until hover and then asked
   for a radio button and an essay; a control nobody can see that then asks for work collects
   nothing. These two are quiet but never invisible — that is the whole difference. */
.rb-fb-quick { display: inline; }
.rb-fb-ask { font-size: var(--t-small); color: var(--ink-3); margin: 0 0 .5rem; }
.rb-fb-choices { display: flex; gap: .4rem; }
.rb-fb-btn {
  background: var(--inset); border: 1px solid var(--edge); border-radius: var(--radius-s);
  color: var(--ink); cursor: pointer; font-size: var(--t-small); line-height: 1.2;
  padding: .35rem .6rem; white-space: nowrap;
  transition: border-color 110ms ease-out, background 110ms ease-out;
}
.rb-fb-works:hover   { border-color: var(--led-good); background: var(--raised); }
.rb-fb-awkward:hover { border-color: var(--estop);    background: var(--raised); }

.rb-fb-thanks {
  border: 1px solid var(--edge); border-left: 3px solid var(--action);
  border-radius: var(--radius-s); background: var(--raised);
  padding: .6rem .75rem; margin-bottom: .6rem; font-size: var(--t-small);
}

/* Endpoint names are words, not numbers, so their columns need room the VLAN grid does not. */
.rb-vm-wide thead th { min-width: 84px; padding: 3px 5px; writing-mode: initial; }
.rb-vm-wide td { width: auto; min-width: 84px; }

/* The new-hull scaffolding. Prominent while it is needed and gone the moment it is not — a
   permanent "getting started" strip becomes furniture people stop reading within a week. */
.rb-setup {
  border: 1px solid var(--action); border-left: 4px solid var(--action);
  border-radius: var(--radius-m); background: var(--raised);
  padding: .85rem 1rem; margin: .75rem 0 1rem;
}
.rb-setup-lead { margin: 0 0 .6rem; font-size: var(--t-small); }
.rb-setup-list { margin: 0; padding-left: 1.2rem; }
.rb-setup-list li { margin-bottom: .55rem; }
.rb-setup-list li a { font-weight: 600; }
.rb-setup-why { color: var(--ink-3); font-size: var(--t-micro); max-width: 68ch; margin-top: .1rem; }
.rb-setup-done > a { font-weight: 400; color: var(--ink-3); text-decoration: line-through; }
.rb-setup-done .rb-setup-why { display: none; }

/* Out of the page container and across the whole window.

   The matrix is the one block on this page that is genuinely wider than the text column it lives
   in: fifty-eight columns do not fit in a measure chosen for reading sentences. The rest of the
   page keeps its width — a comfortable line length is not a limitation to be worked around, it is
   the reason the container exists — and this one block is allowed out. */
/* How far a block may reclaim when it widens: exactly the container's own side padding, so the
   two cannot drift apart. Tabler's .container-xl pads by half its gutter — 1rem at every
   breakpoint this app runs at. */
:root { --rb-bleed: 1rem; }

.rb-vm-wrap { position: relative; }
.rb-vm-wrap.rb-vm-full {
  /* BREAKS OUT OF THE CONTAINER, NOT OUT OF THE WINDOW.

     This was `width: 100vw; margin-left: calc(50% - 50vw)` — the standard full-bleed, which is
     correct only when its container is centred in the viewport. Ours never is: a 240px rail sits
     to the left, so the block landed half a rail too far left, slid under the navigation, and ran
     the same distance off the right-hand edge. Screenshotted 2026-08-22 with the matrix header and
     the shadow-clause banner both cut off mid-word.

     It went unnoticed because widening is a stored per-reader preference — the page looks right
     until someone turns it on, and then it looks broken everywhere else too.

     There is nothing to escape to any more: since the wide-screen steps at the end of this file,
     the container already fills the content area. So the block reclaims the container's own
     padding and stops. No vw, no assumption about what sits beside it, and nothing to re-derive
     if the rail changes width. */
  width: auto;
  margin-inline: calc(-1 * var(--rb-bleed, 1rem));
  padding: 0;
}
.rb-vm-full .rb-vm-scroll { height: max(30rem, 78vh); }

.rb-vm-widen {
  background: none; border: 1px solid var(--edge); border-radius: var(--radius-s);
  color: var(--ink-3); cursor: pointer; font-size: var(--t-micro);
  padding: .2rem .5rem; white-space: nowrap;
}
.rb-vm-widen:hover { border-color: var(--edge-strong); color: var(--ink); background: var(--raised); }

@media print {
  .rb-vm-scroll { height: auto !important; resize: none; }
  .rb-vm-wrap.rb-vm-full { width: auto; margin-inline: 0; padding: 0; }
}

/* ============================ grid cells that used to be drawn by a cellRenderer

   AG Grid 33.0.4 as vendored here never calls a cellRenderer — proven on an isolated page against
   a checksummed copy of the CDN bundle. cellClass, cellClassRules, cellStyle and valueFormatter
   all work. So these three are drawn by styling the cell AG Grid already renders, and the cell's
   own text stays the label — which also means colour is never the only signal, the same promise
   the pill made. */

/* The anchor as label tape: white on near-black, monospaced, the way a Brady label on a cable
   reads. It fills the cell rather than hugging the text, because without a renderer there is no
   element to hug it with — and on an ID column that reads as a tape either way. */
.ag-cell.rb-cell-tape {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11px; letter-spacing: -0.01em;
  background: var(--tape); color: var(--tape-ink);
}
.ag-cell.rb-cell-tape:empty { background: none; }

/* Status as an edge and a tint rather than a pill. A pill that fills the whole cell is a bar
   pretending to be a pill; an edge is what a dense grid wants, and the word is still the value. */
.ag-cell.rb-cell-status { border-left: 3px solid transparent; font-weight: 550; }
/* Every colour rule below carries .ag-cell too. Without it the base rule above is 0,2,0 and these
   are 0,1,0, so `border-left: transparent` wins and the edge never draws — the tint appears and
   the colour that carries the meaning does not. Third time this selector-list trap has cost this
   project an afternoon; the fix is always to score the branches, not to add !important. */
.ag-cell.rb-cell-status-published,
.ag-cell.rb-cell-status-deployed,
.ag-cell.rb-cell-status-granted,
.ag-cell.rb-cell-status-resolved,
.ag-cell.rb-cell-status-closed,
.ag-cell.rb-cell-status-low,
.ag-cell.rb-cell-status-confirmed,
.ag-cell.rb-cell-status-done,
.ag-cell.rb-cell-status-approved {
  border-left-color: var(--led-good); background: var(--led-good-bg); color: var(--led-good);
}
.ag-cell.rb-cell-status-submitted,
.ag-cell.rb-cell-status-pending,
.ag-cell.rb-cell-status-planned,
.ag-cell.rb-cell-status-medium,
.ag-cell.rb-cell-status-partial,
.ag-cell.rb-cell-status-awaiting-qc {
  border-left-color: var(--annunciator); background: var(--annunciator-bg); color: var(--annunciator);
}
.ag-cell.rb-cell-status-draft,
.ag-cell.rb-cell-status-open,
.ag-cell.rb-cell-status-not-submitted,
.ag-cell.rb-cell-status-considered {
  border-left-color: var(--ink-4); background: var(--anodised); color: var(--ink-3);
}
.ag-cell.rb-cell-status-critical,
.ag-cell.rb-cell-status-high,
.ag-cell.rb-cell-status-rejected,
.ag-cell.rb-cell-status-sent-back,
.ag-cell.rb-cell-status-divergent,
.ag-cell.rb-cell-status-declined,
.ag-cell.rb-cell-status-rolled-back {
  border-left-color: var(--estop); background: var(--estop-bg); color: var(--estop);
}

/* The manufacturer's mark, in front of the name. A platform whose file is missing keeps the name
   and loses only the picture — the same fallback the renderer had. */
.ag-cell.rb-cell-vendor {
  background-repeat: no-repeat; background-position: 2px center; background-size: auto 13px;
  padding-left: 30px;
}

/* Something gets through this pair even though the segment is not open. A corner wedge rather
   than another colour: the palette already carries the verdict, and a fifth hue on a 15px cell
   would compete with the two that mean a finding. Small, consistent, and findable by scanning. */
.rb-vm-exc { position: relative; }
.rb-vm-exc::after {
  content: ""; position: absolute; right: 0; top: 0;
  border-width: 0 5px 5px 0; border-style: solid;
  border-color: transparent var(--ink-3) transparent transparent;
}
.rb-vm-legend-exc {
  display: inline-block; width: 11px; height: 11px; border-radius: 2px;
  background: var(--anodised); position: relative; vertical-align: -1px; margin-right: .3rem;
}
.rb-vm-legend-exc::after {
  content: ""; position: absolute; right: 0; top: 0;
  border-width: 0 5px 5px 0; border-style: solid;
  border-color: transparent var(--ink-3) transparent transparent;
}

/* =====================================================================================
   WIDE SCREENS — the page grows, the prose does not

   Reported 2026-08-22 with a screenshot of a ~2000px window: "ik heb rechts en links van
   last config read bijv nog heel veel ruimte". Two caps were stacking. Tabler pins
   .container-xl at 1320px and style.css pinned `main` at the same figure, so past about
   1600px of window every extra pixel became margin. On the project page that is roughly 440px
   of nothing on either side of a 97-rule list and a 58-VLAN matrix — the two things on this
   product that are hungriest for width.

   A max-width earns its place on RUNNING TEXT, where a line longer than ~75 characters is
   measurably harder to read. It earns nothing on a grid. So the cap moves off the page and
   stays on the prose: `p`, `.rb-lead` and `.intent` already carry their own ch-based limits,
   which is why widening the container here does not produce 200-character sentences.

   Two steps rather than one, and no `100%`: a 2560px monitor showing a full-bleed table turns
   a scan into a head-turn, and the eye loses the row. 1560 then 1760 keeps the longest row
   inside a comfortable sweep while giving back nearly all the space that was being wasted.
   ===================================================================================== */

@media (min-width: 1600px) {
  .page-body > .container-xl,
  .page-header > .container-xl,
  main { max-width: 1560px; }
}

@media (min-width: 1900px) {
  .page-body > .container-xl,
  .page-header > .container-xl,
  main { max-width: 1760px; }
}
