/* Ultra Shell — UltraCNS universal application shell (arrangement 2a/3a).
   Self-contained: fonts, tokens, shell chrome, and the dashboard primitives.
   Drop the whole ultra-shell/ folder into a project and link this file. */

@font-face{font-family:"Space Grotesk";src:url("fonts/space-grotesk-var-latin.woff2") format("woff2");font-weight:300 700;font-style:normal;font-display:swap}
@font-face{font-family:"DM Sans";src:url("fonts/dm-sans-var-latin.woff2") format("woff2");font-weight:300 700;font-style:normal;font-display:swap}
/* Chivo and IBM Plex Mono, bundled 2026-09-09 so the interface matches the
   skin it was chosen from. Both SIL OFL 1.1 - see fonts/LICENCES.md. Local
   files, never a CDN: the shell has to render with no network, and nothing
   outside should learn which site is being looked at. */
@font-face{font-family:"Chivo";src:url("fonts/chivo-var-latin.woff2") format("woff2");font-weight:300 700;font-style:normal;font-display:swap}
@font-face{font-family:"IBM Plex Mono";src:url("fonts/ibm-plex-mono-400-latin.woff2") format("woff2");font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:"IBM Plex Mono";src:url("fonts/ibm-plex-mono-500-latin.woff2") format("woff2");font-weight:500;font-style:normal;font-display:swap}
/* ── the sign-in kit's two faces ───────────────────────────────────────────
   Bebas Neue and Barlow, the UltraCNS sign-in look, local rather than
   fetched. signin.html and setup.html pulled these from Google on every load
   -- see the header of signin.css. Latin subsets, from Google's own woff2,
   both SIL OFL 1.1.

   Weights are the ones these pages actually ask for: 300 for the hero
   paragraph, 400, 600, and 700. The 650 the form heading asks for is
   synthesised from 700 by the browser as it always was -- Barlow ships no
   650, and Google was never sending one either. */
@font-face{font-family:"Bebas Neue";src:url("fonts/bebas-neue-400-latin.woff2") format("woff2");font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:"Barlow";src:url("fonts/barlow-300-latin.woff2") format("woff2");font-weight:300;font-style:normal;font-display:swap}
@font-face{font-family:"Barlow";src:url("fonts/barlow-400-latin.woff2") format("woff2");font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:"Barlow";src:url("fonts/barlow-600-latin.woff2") format("woff2");font-weight:600;font-style:normal;font-display:swap}
@font-face{font-family:"Barlow";src:url("fonts/barlow-700-latin.woff2") format("woff2");font-weight:700;font-style:normal;font-display:swap}


:root{
  /* ---- density ----
     The shell was written for a page somebody reads. Some applications are a
     page somebody WORKS in all day and want more on screen at once. These are
     the values the package has always used, so nothing moves by naming them;
     an application that needs to be denser sets its own on .ultra-shell rather
     than editing this file, which is what UltraDesk does per screen. */
  /* Density, 2026-08-30. Measured before: a single-line Wi-Fi row was 85px and a
     Systems row 123px, so a 900px screen held ten rows. The padding was doing
     most of that, not the content. These are the only numbers that need to move
     - every table, card and grid reads them, so density stays consistent across
     the applications instead of being tuned page by page. Type sizes are NOT
     reduced: the point is to remove dead space, not to make things harder to
     read. */
  --ultra-pad-page:20px; --ultra-gap-block:14px; --ultra-gap-grid:12px;
  --ultra-pad-card:14px 18px; --ultra-pad-cardhead:11px 18px; --ultra-gap-card:10px;
  --ultra-pad-row:7px 18px; --ultra-pad-thead:7px 18px;
  --ultra-row-line:1.45;

/* ================================================================
   TYPE SCALE                                          2026-09-09
   ================================================================
   Before this, the package had four size tokens and 123 hard-coded
   declarations: eleven sizes between 10.5px and 34px, five weights
   with no rule about which meant what, and thirteen letter-spacings
   including `.14em`, `0.14em`, `.2em`, `0.20em` and `0.2em` - the
   same value written three ways. Sections looked independently
   styled because they were.

   Eight steps now, each with one job. The name says the ROLE, not
   the size, so a screen is written by asking what a line is rather
   than picking a number that looks about right:

     display  34   the page title, and the figure a page exists for
     head     22   a drawer, dialog or hero panel title
     title    20   a card title
     figure   20   a stat value inside a card
     lede     14   the reading size - a sentence you actually read,
                   and anything you type into or click in the nav
     body     13   dense interface text: rows, buttons, chips, links
     meta     12   the second line under a name - an IP, a label
     fine     11   footnotes, captions, chart axes, counts
     label   10.5  uppercase and tracked: column heads, field labels

   Two things stay outside the scale on purpose. The wordmark is a
   mark, not text, and the drawer's close glyph is an icon that
   happens to be a character. Everything else reads a token.

   Weight carries the same discipline: --ultra-w-display for
   anything in the display face, --ultra-w-label for the tracked
   uppercase role, and inherited 400 everywhere else. A skin can
   restate both - Abyssal draws its figures at 300 - and the whole
   application follows, because nothing hard-codes a weight. */
  --ultra-t-display:34px; --ultra-t-head:22px; --ultra-t-title:20px;
  --ultra-t-figure:20px;  --ultra-t-lede:14px; --ultra-t-body:13px;
  --ultra-t-meta:12px;    --ultra-t-fine:11px; --ultra-t-label:10.5px;
  --ultra-w-display:700;  --ultra-w-label:600;  --ultra-w-mark:700;
  --ultra-track-display:-0.02em; --ultra-track-label:0.14em;
  --ultra-lh-display:1.06; --ultra-lh-head:1.2; --ultra-lh-text:1.6;
  --ultra-lh-control:1.5; --ultra-lh-tight:1.35; --ultra-lh-pre:1.7;

/* The reading measure. Prose set the full width of the content area ran to
   about 230 characters a line on a wide screen, which is roughly three
   times what an eye tracks comfortably: the return sweep loses the next
   line. Every role that carries a SENTENCE is capped; table cells, labels
   and figures are not, because they are read by scanning down a column
   rather than across a line. */
  --ultra-measure:90ch;

/* Faces by role, so a skin changes what data is SET IN by restating one
   token rather than listing every selector that carries data. display is
   titles and figures; body is everything read; data is addresses, numbers
   and times - the things a person compares down a column; label is the
   tracked uppercase role. Standard sets data in the display face at 500
   (not 700 - a column of bold IPs was the loudest thing on the page);
   Abyssal sets data and label in monospace. */
  --ultra-font-display:"Chivo","Space Grotesk",sans-serif; --ultra-font-body:"DM Sans",sans-serif;
  --ultra-font-data:"IBM Plex Mono",ui-monospace,monospace;
  --ultra-font-label:"IBM Plex Mono",ui-monospace,monospace;
  --ultra-w-data:500; --ultra-w-name:500;

/* Column widths by KIND of column. A status column is the same width on
   every page because it is the same column; before this, 37 tables carried
   31 different templates and status alone was 110, 120, 130, 140 or 150px
   depending on which page you were on. A page composes its template from
   these - `columns('name addr mac text status time')` in Nexus - and never
   writes a pixel. Widths are set from the content at the data size: an IPv4
   address, a MAC, a byte figure, a duration, the widest badge. */
  --ultra-col-check:44px;          --ultra-col-name:minmax(180px,1.6fr);
  --ultra-col-text:minmax(120px,1fr); --ultra-col-wide:minmax(240px,2fr);
  --ultra-col-meas:minmax(120px,1fr);
  /* A reading column that carries several figures at once, the way acts
     has actswide. The Systems row lists what SNMP reports beside loss and
     latency, which does not fit the single-figure width. */
  --ultra-col-measwide:minmax(240px,2fr);
  --ultra-col-addr:150px;          --ultra-col-mac:180px;
  --ultra-col-num:96px;            --ultra-col-time:150px;
  /* Every track is a fixed width or a fraction - never content-sized. The
     head and each row are separate grids, so a `max-content` track resolves
     from that row's OWN content: the head measured the word ACTION while a
     row measured the button "Unacknowledge", the leftover the fractions
     shared came out different, and every column drifted a little further
     from its heading down the table. A row that needs more room for its
     buttons says so with --acts on the table. */
  /* Two actions widths, because a row of five text buttons is a different
     column from a row of one, and both have to be a FIXED width: the head
     and each row are separate grids, so a content-sized track resolves from
     that row's own content - the head measured "ACTION" while a row measured
     "Unacknowledge" - and every fraction beside it drifted. Pick by how many
     buttons the row carries; nothing here reads a per-table variable, which
     is what the previous attempt did and it silently never applied. */
  --ultra-col-status:128px;        --ultra-col-acts:132px;
  --ultra-col-actswide:400px;
  /* A matrix column: many across, so they share the width rather than
     each claiming a fixed one - a fixed num column ten times over is how
     the flow matrix came to scroll sideways. */
  --ultra-col-cell:minmax(64px,1fr);
  --ultra-pad-x:18px; --navy-900:#0b1c2c; --navy-850:#0e2436; --navy-600:#4a5863; --navy-blue-800:#0e2944; --navy-blue-700:#113556;
  --blue-700:#0f5da8; --blue-600:#146ec2; --blue-500:#1a7ed3; --blue-400:#4e9bdb;
  --blue-300:#7cbdf0; --blue-100:#e3f0fb; --blue-050:#f0f7fd;
  --cream-50:#f5f1ea; --cream-100:#efeae1; --white:#fff;
  --ink-900:#0b1c2c; --ink-800:#16293a; --ink-700:#2a3b49; --ink-500:#56646f;
  --ink-400:#7c8892; --ink-300:#a8b1b8; --ink-100:#e8e4dd;
  --alpha-white-08:rgba(255,255,255,0.08); --alpha-white-12:rgba(255,255,255,0.12);
  --alpha-white-52:rgba(246,243,238,0.52); --alpha-white-70:rgba(246,243,238,0.70);
  --alpha-ink-10:rgba(11,28,44,0.10); --alpha-ink-14:rgba(11,28,44,0.14);
  --alpha-blue-35:rgba(26,126,211,0.35);
  /* RAG - Threat Grid. Two colours per status, because one cannot do both jobs:
     the text colour has to stay legible on a light card, and the mark colour has
     to be bright enough to read at a glance. Measured on white: the vivid amber
     is 2.1:1 and would fail as text, which is why it is never used as text.
     Chips are solid with their own ink - dark on teal and amber, white on red -
     measured 7.58, 6.92 and 5.22:1, all clear of AA. */
  --status-positive:#00705c; --status-attention:#965900; --status-critical:#c62b21;
  --status-positive-vivid:#00c8a0; --status-attention-vivid:#ff9f0a; --status-critical-vivid:#ff453a;
  --status-positive-rgb:0,200,160; --status-attention-rgb:255,159,10; --status-critical-rgb:255,69,58;

  --ultra-rail:248px; --ultra-rail-collapsed:68px; --ultra-topbar:60px;
  --ultra-r-chip:8px; --ultra-r-sm:10px; --ultra-r-card:14px; --ultra-r-panel:18px;
  --ultra-ease:cubic-bezier(0.2,0,0.2,1);
}

/* ---------- shell chrome ---------- */
/* position:relative is load-bearing, not tidiness. The lit ground below
   over-scans by 25% so the blur has no hard edge, and with the shell static
   that pseudo-element resolved against the viewport instead - outside the
   shell's own overflow:hidden, which therefore clipped nothing. The page then
   measured 1.25x the window and scrolled sideways by a quarter of its width:
   360px at 1440, and 166 of 663 measured here on 2026-09-09. Every overlay is
   position:fixed and a relative ancestor does not touch fixed positioning, so
   nothing else moves. */
.ultra-shell{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--cream-50);
  position:relative;
  font-family:var(--ultra-font-body);color:var(--ink-800);overflow:hidden}
.ultra-shell *{box-sizing:border-box}
.ultra-shell a{color:var(--blue-600);text-decoration:none}
.ultra-shell a:hover{color:var(--blue-700)}
.ultra-shell :focus-visible{outline:none;box-shadow:0 0 0 3px var(--alpha-blue-35)}

.ultra-topbar{flex:none;height:var(--ultra-topbar);background:var(--navy-900);border-bottom:1px solid var(--alpha-white-08);
  display:flex;align-items:center;gap:24px;padding:0 24px}
.ultra-logo{display:flex;align-items:baseline;gap:16px;flex:none;padding-right:8px}
.ultra-logo__shield{height:26px;width:auto;flex:none;display:block}
.ultra-logo__lockup{height:30px;width:auto;flex:none;display:block}
.ultra-logo__word{font-family:var(--ultra-font-display);font-size:18px;font-weight:var(--ultra-w-mark);letter-spacing:0.01em;color:#fff;white-space:nowrap}
.ultra-logo__word-accent{color:var(--blue-400)}   /* 4.08:1 on navy -> 5.78:1 */
.ultra-logo__product{font-size:var(--ultra-t-label);letter-spacing:var(--ultra-track-label);text-transform:uppercase;color:var(--alpha-white-52);white-space:nowrap}
.ultra-switcher{width:196px;flex:none;display:flex;align-items:center;justify-content:space-between;gap:8px;
  background:var(--navy-blue-800);border:1px solid var(--alpha-blue-35);border-radius:var(--ultra-r-chip);
  padding:7px 12px;color:#fff;font-size:var(--ultra-t-body);cursor:pointer}
.ultra-switcher:hover{background:var(--navy-blue-700)}
.ultra-switcher span:last-child{color:var(--blue-300)}
.ultra-search{flex:1;max-width:420px;background:var(--navy-850);border:1px solid var(--alpha-white-12);
  border-radius:var(--ultra-r-chip);padding:8px 12px;font-size:var(--ultra-t-body);color:var(--alpha-white-52);cursor:text;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.ultra-topbar__spacer{flex:1}
.ultra-topbar__right{display:flex;align-items:center;gap:18px;font-size:var(--ultra-t-body);color:var(--alpha-white-70)}
.ultra-topbar__right button{background:none;border:0;padding:0;font:inherit;color:inherit;cursor:pointer}
.ultra-topbar__right button:hover{color:#fff}
.ultra-user{display:flex;align-items:center;gap:8px;color:#fff}
.ultra-avatar{width:26px;height:26px;border-radius:999px;background:var(--blue-600);color:#fff;font-size:var(--ultra-t-fine);
  font-family:var(--ultra-font-display);font-weight:var(--ultra-w-mark);display:flex;align-items:center;justify-content:center}

.ultra-body{flex:1;min-height:0;display:grid;grid-template-columns:var(--ultra-rail) 1fr;
  transition:grid-template-columns 200ms var(--ultra-ease)}
.ultra-shell[data-collapsed="true"] .ultra-body{grid-template-columns:var(--ultra-rail-collapsed) 1fr}

/* The column fills its cell and never scrolls as a whole; the NAV inside it
   scrolls and the foot is pinned. Scrolling the whole column - which is what
   overflow-y:auto here did - took Settings and Collapse off the bottom of the
   screen on any window short enough to need it, and left the nav list running
   off the edge with nothing under it. */
.ultra-sidebar{background:var(--white);border-right:1px solid var(--alpha-ink-10);padding:20px 12px;
  display:flex;flex-direction:column;gap:20px;overflow:hidden;min-height:0}
.ultra-nav{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;
  display:flex;flex-direction:column;gap:20px;scrollbar-width:thin}
/* The foot keeps its own hairline, so it still reads as separated from the nav
   whether or not the list above it has scrolled. */
.ultra-navfoot{flex:none}

/* The build stamp. Quiet by design - it is reference information, not a status,
   so it sits below the foot buttons at the smallest legible size and never
   competes with the nav. Hidden entirely when the application has not set one. */
.ultra-build{padding:8px 14px 2px;font-size:var(--ultra-t-label);letter-spacing:var(--ultra-track-label);
  color:var(--ink-400);font-variant-numeric:tabular-nums;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
.ultra-build[hidden]{display:none}
.ultra-shell[data-collapsed="true"] .ultra-build{display:none}
.ultra-shell[data-collapsed="true"] .ultra-sidebar{padding:20px 10px}
.ultra-navgroup{display:flex;flex-direction:column;gap:2px}
.ultra-navgroup--fill{flex:1}
.ultra-navgroup__label{font-size:var(--ultra-t-label);font-weight:var(--ultra-w-label);letter-spacing:var(--ultra-track-label);text-transform:uppercase;color:var(--ink-500);
  padding:0 10px 6px;white-space:nowrap}
.ultra-navitem{display:flex;align-items:center;gap:12px;padding:8px 10px;border-radius:var(--ultra-r-chip);
  font-size:var(--ultra-t-lede);color:var(--ink-700);white-space:nowrap;cursor:pointer;background:none;border:0;font-family:inherit;text-align:left;width:100%}
.ultra-navitem svg{flex:none;color:var(--ink-400)}
.ultra-navitem:hover{background:var(--cream-100)}
.ultra-navitem[data-active="true"]{background:var(--blue-050);color:var(--blue-700)}
.ultra-navitem[data-active="true"] svg{color:var(--blue-600)}
.ultra-navfoot{border-top:1px solid var(--alpha-ink-10);padding-top:12px;display:flex;flex-direction:column;gap:2px;flex:none}
.ultra-collapse{font-size:var(--ultra-t-body);color:var(--ink-500)}
.ultra-shell[data-collapsed="true"] .ultra-navgroup__label{visibility:hidden;height:0;padding:0}
.ultra-shell[data-collapsed="true"] .ultra-navitem{justify-content:center}
.ultra-shell[data-collapsed="true"] .ultra-navitem__label{display:none}

.ultra-content{min-width:0;overflow:auto;padding:var(--ultra-pad-page);display:flex;flex-direction:column;gap:var(--ultra-gap-block)}
.ultra-content > *{flex:0 0 auto}

/* ---------- page furniture ---------- */
/* Wraps. A page with six window buttons and a long title overflowed the
   content column and gave the whole page a horizontal scrollbar - the header
   was the only thing in the layout that could not give. */
.ultra-pagehead{display:flex;align-items:flex-end;justify-content:space-between;gap:16px 24px;
  flex-wrap:wrap}
.ultra-pagehead__text{flex:1 1 260px}
.ultra-pagehead__text{display:flex;flex-direction:column;gap:6px}
.ultra-eyebrow{font-size:var(--ultra-t-label);font-weight:var(--ultra-w-label);letter-spacing:var(--ultra-track-label);text-transform:uppercase;color:var(--blue-600)}
.ultra-title{margin:0;font-family:var(--ultra-font-display);font-size:var(--ultra-t-display);font-weight:var(--ultra-w-display);
  letter-spacing:var(--ultra-track-display);line-height:var(--ultra-lh-display);color:var(--ink-900)}
.ultra-sub{margin:0;font-size:var(--ultra-t-lede);line-height:var(--ultra-lh-text);color:var(--ink-500);max-width:var(--ultra-measure)}
.ultra-actions{display:flex;flex-wrap:wrap;gap:10px;flex:0 1 auto;flex-wrap:wrap;justify-content:flex-end}
.ultra-btn{font-family:inherit;font-size:var(--ultra-t-body);color:var(--ink-700);background:var(--white);border:1px solid var(--alpha-ink-14);
  border-radius:var(--ultra-r-chip);padding:10px 16px;cursor:pointer;white-space:nowrap}
.ultra-btn:hover{background:var(--cream-100)}
.ultra-btn:active{transform:scale(0.99)}
/* White on --blue-500 measures 4.23:1, under the 4.5 AA floor - the primary
   button has been failing since the package shipped. --blue-600 is 5.2:1.
   Found by the UltraCRM session and recorded in its ultra-shell-upstream.md;
   verified here before adopting. */
.ultra-btn--primary:hover{background:var(--blue-700)}
/* A link styled as a button is still a link, and `.ultra-shell a` (0,1,1) beat
   `.ultra-btn--primary` (0,1,0), so the label took the link colour and rendered
   blue on blue at 1.23:1 - invisible, and it shipped that way. These match the
   link selector's specificity so a button reads as a button whatever the tag. */
.ultra-shell a.ultra-btn{color:var(--ink-800)}
.ultra-shell a.ultra-btn:hover{color:var(--ink-900)}
.ultra-shell a.ultra-btn--primary,
.ultra-shell a.ultra-btn--primary:hover{color:#fff}
.ultra-shell a.ultra-btn--danger,
.ultra-shell a.ultra-btn--danger:hover{color:#fff}

/* ---------- text roles ----------
   Six classes covering everything below a heading. They exist so a view can
   say what a line IS - a lede, a note, a name's second line - instead of
   picking a size and a grey and hoping it matches the screen next door. */
.ultra-lede{font-size:var(--ultra-t-lede);line-height:var(--ultra-lh-text);color:var(--ink-500);max-width:var(--ultra-measure)}
.ultra-note{font-size:var(--ultra-t-body);line-height:var(--ultra-lh-text);color:var(--ink-500);max-width:var(--ultra-measure)}
.ultra-meta{font-size:var(--ultra-t-meta);line-height:var(--ultra-row-line);color:var(--ink-500);max-width:var(--ultra-measure)}
.ultra-fine{font-size:var(--ultra-t-fine);line-height:var(--ultra-lh-text);color:var(--ink-400);max-width:var(--ultra-measure)}
.ultra-label{font-size:var(--ultra-t-label);font-weight:var(--ultra-w-label);
  letter-spacing:var(--ultra-track-label);text-transform:uppercase;color:var(--ink-400)}
.ultra-figure{font-family:var(--ultra-font-display);font-size:var(--ultra-t-figure);
  font-weight:var(--ultra-w-display);letter-spacing:var(--ultra-track-display);
  line-height:var(--ultra-lh-head);color:var(--ink-900)}
/* Everything that IS the label role, in one place, reading the label face.
   A skin that wants column heads in monospace sets --ultra-font-label and
   every one of these follows; it does not get to pick which. */
.ultra-label,.ultra-eyebrow,.ultra-table__head,.ultra-field__label,.ultra-kv dt,.ultra-prose h4,
.ultra-navgroup__label,.ultra-results__group,.ultra-modmenu__label,.ultra-menu__head,.ultra-badge,
.ultra-build,.ultra-metric__label,.ultra-metric__note,.ultra-chart__axis,.ultra-heat__lab{
  font-family:var(--ultra-font-label)}

/* ---------- metrics ---------- */
.ultra-metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--ultra-gap-grid)}
.ultra-metric{  padding:var(--ultra-pad-card);display:flex;flex-direction:column;gap:6px}
.ultra-metric__value{font-family:var(--ultra-font-display);font-size:var(--ultra-t-display);font-weight:var(--ultra-w-display);
  letter-spacing:var(--ultra-track-display);line-height:var(--ultra-lh-display);color:var(--ink-900)}
.ultra-metric__label{font-size:var(--ultra-t-meta);color:var(--ink-500)}
/* The accent metric used to be a navy card sitting among white ones, which read
   as a different KIND of tile rather than an emphasised one. It is an ordinary
   card now. The modifier is kept so no application has to change its calls. */
.ultra-metric--accent{background:var(--white);border-color:var(--ink-100)}
.ultra-metric--accent .ultra-metric__value{color:var(--ink-900)}
.ultra-metric--accent .ultra-metric__label{color:var(--ink-500)}
.ultra-metric__value--attention{color:var(--status-attention)}
.ultra-metric__value--critical{color:var(--status-critical)}
.ultra-metric__value--positive{color:var(--status-positive)}

/* ---------- cards, tables, badges, chips ---------- */
.ultra-card{overflow:clip}
.ultra-card--pad{padding:var(--ultra-pad-card);display:flex;flex-direction:column;gap:var(--ultra-gap-card)}
/* A table inside a padded card was indenting twice: the card's 18px, then the
   row's own 18px, so rows sat 36px in while the title sat at 18. The table
   goes full bleed and its rows' padding lands on the card's own text edge -
   names line up with the title, and the dividers reach both edges, which is
   what makes a list read as one column rather than a box inside a box. */
.ultra-card--pad > .ultra-table,
.ultra-card--pad > .ultra-inset--row{margin-left:calc(-1 * var(--ultra-pad-x));
  margin-right:calc(-1 * var(--ultra-pad-x))}
.ultra-card__head{display:flex;align-items:center;justify-content:space-between;gap:16px;
  padding:var(--ultra-pad-cardhead);border-bottom:1px solid var(--alpha-ink-10)}
.ultra-card__title{font-family:var(--ultra-font-display);font-size:var(--ultra-t-title);font-weight:var(--ultra-w-display);
  letter-spacing:var(--ultra-track-display);line-height:var(--ultra-lh-head);color:var(--ink-900)}
.ultra-card__link{font-size:var(--ultra-t-body);color:var(--blue-600);cursor:pointer}

.ultra-table{--cols:1fr}
/* The gap is on the head AND the row, so a right-aligned head never runs
   into its neighbour: MBPSHEALTH was a head with no gap over rows with one. */
.ultra-table__head,.ultra-table__row{display:grid;grid-template-columns:var(--cols);column-gap:16px}
/* Sticky, so a long table keeps its column names while you scroll it. It needs
   .ultra-card to clip rather than hide, or the card becomes the scroll context
   and the header sticks to nothing. */
.ultra-table__head{background:var(--navy-900);color:#fff;font-size:var(--ultra-t-label);font-weight:var(--ultra-w-label);letter-spacing:var(--ultra-track-label);
  text-transform:uppercase;padding:var(--ultra-pad-thead);position:sticky;top:0;z-index:3}
/* Height comes from the content: single-line rows stay compact, a wrapped cell
   grows the row on its own. align-items:center keeps text, badges, buttons and
   checkboxes on one baseline whatever their individual heights. */
.ultra-table__row{padding:var(--ultra-pad-row);border-bottom:1px solid var(--alpha-ink-10);font-size:var(--ultra-t-body);
  color:var(--ink-700);align-items:center;line-height:var(--ultra-row-line);
  }
.ultra-table__row:last-child{border-bottom:0}
.ultra-table__row--marked{background:var(--blue-050)}
/* A group header inside a table: the same row, separated by a stronger hairline
   above rather than by a fill, so a long discovery result reads as sections
   without any of them becoming a box. */
.ultra-table__row--group{border-top:1px solid var(--alpha-ink-14)}
.ultra-table__row--group:first-child{border-top:0}
/* A fault among the readings. It pulses for the same reason the topology map
   pulses an arrival: it is the one thing in a row of healthy numbers worth
   catching an eye that is looking somewhere else, and a row where "RAID
   degraded" is set in the same quiet ink as "cpu 4%" hides it in plain sight.

   The floor is 0.5 rather than 0, so the word stays readable at every point in
   the cycle - a fault that is invisible for half a second at a time is worse
   than one that does not move. Reduced motion gets the colour and no movement,
   which is still the whole of the meaning. */
.ultra-fault{color:var(--status-critical);font-weight:var(--ultra-w-label);
  animation:ultra-fault 1.4s ease-in-out infinite}
@keyframes ultra-fault{0%,100%{opacity:1} 50%{opacity:.5}}
@media (prefers-reduced-motion:reduce){.ultra-fault{animation:none}}

/* A device inside a group. The group bar and the row it contains share a left
   edge, so a device sat exactly inline with the name of the group holding it
   and the tree read flat. 46px is that group bar's own disclosure arrow, grip
   and gaps added up, so a device name starts precisely where its group's name
   starts - the step is the shell's existing measurements, not a new number.
   Only the name cell moves; address, readings and state are separate grid
   columns and keep their widths. */
.ultra-table__row[data-drag="host"] .ultra-cell--name,
.ultra-table__row[data-drop="empty"] .ultra-cell--text{padding-left:46px}

/* A row belonging to the row above it: the individual readings behind a metric
   that has several. Indented by the width of its own checkbox so the tick
   column still lines up, and quieter, because the parent is the thing being
   chosen and these are what it is made of. */
.ultra-table__row--sub .ultra-cell--name{padding-left:18px;color:var(--ink-500)}
.ultra-num{font-family:var(--ultra-font-data);font-weight:var(--ultra-w-data);color:var(--ink-900);
  font-variant-numeric:tabular-nums}
/* Copyable values. Quiet until hovered - an affordance on every address in a
   table would shout louder than the data. The hint is the cursor and the tint,
   not an icon competing for the column's width. */
[data-copy]{cursor:copy;border-radius:4px;padding:0 3px;margin:0 -3px;
  transition:background .12s}
[data-copy]:hover{background:var(--blue-050);box-shadow:inset 0 0 0 1px var(--alpha-blue-35)}
.ultra-muted{color:var(--ink-500)}

.ultra-badge{display:inline-block;font-size:var(--ultra-t-label);font-weight:var(--ultra-w-label);letter-spacing:var(--ultra-track-label);text-transform:uppercase;
  border-radius:999px;padding:2px 9px;line-height:var(--ultra-lh-control);background:var(--cream-100);color:var(--ink-700);
  vertical-align:middle;white-space:nowrap}

.ultra-caret{color:var(--blue-500)}

.ultra-meter{height:10px;border-radius:999px;background:var(--cream-100);overflow:hidden}
.ultra-meter__fill{height:100%;background:var(--blue-500)}

.ultra-dot--positive{color:var(--status-positive-vivid)}
.ultra-dot--attention{color:var(--status-attention-vivid)}
.ultra-dot--critical{color:var(--status-critical-vivid)}

/* Navy emphasis panel (the system's inversion device) */
.ultra-panel-navy{background:var(--navy-900);border-radius:var(--ultra-r-card);padding:22px 24px;
  display:flex;align-items:center;gap:24px;color:#fff}
.ultra-panel-navy__title{font-family:var(--ultra-font-display);font-size:var(--ultra-t-head);font-weight:var(--ultra-w-display);
  letter-spacing:var(--ultra-track-display);line-height:var(--ultra-lh-head);color:#fff}
.ultra-panel-navy__meta{font-size:var(--ultra-t-body);color:var(--alpha-white-70)}
.ultra-btn--ghost-dark{background:none;color:#fff;border:1px solid var(--alpha-white-12)}
.ultra-btn--ghost-dark:hover{background:var(--alpha-white-08)}

/* =====================================================================
   Added 2026-08-30 with the module switcher, palette, theme and counts.
   All four are inert unless the application asks for them, so the eight
   products that predate this file render exactly as they did before.
   ===================================================================== */


.ultra-iconbtn{display:flex;align-items:center;justify-content:center;width:30px;height:30px;
  border-radius:var(--ultra-r-chip);background:none;border:0;color:var(--alpha-white-70);cursor:pointer;padding:0}
.ultra-iconbtn:hover{background:var(--alpha-white-08);color:#fff}

/* ---------- search ----------
   A real field in the top bar with its results underneath it. It used to be a
   button that opened a dialog, which made "Search devices, clients, tunnels,
   countries" a promise the control did not keep: it listed pages. */
.ultra-searchwrap{position:relative;flex:1;max-width:420px;min-width:0}
.ultra-search{display:flex;align-items:center;gap:9px;background:var(--navy-850);
  border:1px solid var(--alpha-white-12);border-radius:var(--ultra-r-chip);padding:0 12px;
  height:36px;color:var(--alpha-white-52);max-width:none;flex:none}
.ultra-search:focus-within{border-color:var(--alpha-blue-35);background:var(--navy-blue-800)}
.ultra-search svg{flex:none}
.ultra-search input{flex:1;min-width:0;background:none;border:0;outline:none;
  font-family:inherit;font-size:var(--ultra-t-body);color:#fff;padding:0}
.ultra-search input::placeholder{color:var(--alpha-white-52)}
.ultra-search__key{font-size:var(--ultra-t-label);letter-spacing:var(--ultra-track-label);color:var(--alpha-white-52);
  border:1px solid var(--alpha-white-12);border-radius:5px;padding:1px 6px;white-space:nowrap;flex:none}
.ultra-search:focus-within .ultra-search__key{display:none}

/* Wider than the field it hangs from: a device name plus its group does not
   fit in 420px, and truncating the very thing being searched for defeats the
   search. */
.ultra-results{position:absolute;top:calc(100% + 8px);left:0;width:max(100%,540px);z-index:70;
  background:var(--white);border:1px solid var(--alpha-ink-14);border-radius:var(--ultra-r-sm);
  padding:6px;max-height:min(60vh,520px);overflow-y:auto;
  display:flex;flex-direction:column;gap:1px}
.ultra-results[hidden]{display:none}
.ultra-results__group{font-size:var(--ultra-t-label);font-weight:var(--ultra-w-label);letter-spacing:var(--ultra-track-label);text-transform:uppercase;
  color:var(--ink-400);padding:9px 10px 5px}
.ultra-results__row{display:flex;align-items:center;gap:10px;width:100%;padding:8px 10px;
  border:0;border-radius:var(--ultra-r-chip);background:none;font-family:inherit;font-size:var(--ultra-t-body);
  color:var(--ink-800);text-align:left;cursor:pointer}
.ultra-results__row svg{flex:none;color:var(--ink-400)}
.ultra-results__row[data-on="true"]{background:var(--blue-050);color:var(--blue-700)}
.ultra-results__row[data-on="true"] svg{color:var(--blue-600)}
.ultra-results__t{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ultra-results__m{margin-left:auto;font-size:var(--ultra-t-fine);color:var(--ink-400);white-space:nowrap;
  padding-left:12px}
.ultra-results__none{padding:14px 12px;font-size:var(--ultra-t-body);color:var(--ink-500)}
.ultra-results__foot{border-top:1px solid var(--alpha-ink-10);margin-top:4px;padding:8px 10px 4px;
  font-size:var(--ultra-t-fine);color:var(--ink-400)}

/* ---------- module switcher ---------- */
.ultra-switcher--mod{gap:10px;justify-content:flex-start;font-family:inherit}
.ultra-switcher--mod span:nth-of-type(1){flex:1;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ultra-switcher--mod svg{flex:none;color:var(--blue-300)}
/* Anchored to the switcher, not to a measured offset from the logo: the logo's
   width changes with the product name, and a magic number would drift the
   moment another application used a longer one. */
.ultra-modwrap{position:relative;flex:none}
.ultra-modmenu{position:absolute;top:calc(100% + 12px);left:0;z-index:60;width:396px;
  background:var(--white);border:1px solid var(--alpha-ink-14);border-radius:var(--ultra-r-panel);padding:16px}
.ultra-modmenu__label{font-size:var(--ultra-t-label);font-weight:var(--ultra-w-label);letter-spacing:var(--ultra-track-label);text-transform:uppercase;
  color:var(--ink-400);padding:0 4px 10px}
.ultra-modgrid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px}
.ultra-modtile{display:flex;flex-direction:column;gap:4px;align-items:flex-start;padding:14px;
  border:1px solid var(--alpha-ink-10);border-radius:var(--ultra-r-sm);background:none;
  font-family:inherit;font-size:var(--ultra-t-lede);color:var(--ink-800);cursor:pointer;text-align:left}
.ultra-modtile:hover{background:var(--cream-100)}
.ultra-modtile svg{color:var(--ink-400)}
.ultra-modtile small{font-size:var(--ultra-t-fine);color:var(--ink-400)}
.ultra-modtile[data-active="true"]{background:var(--blue-050);border-color:var(--alpha-blue-35);color:var(--blue-700)}
.ultra-modtile[data-active="true"] svg{color:var(--blue-600)}

/* ---------- live counts on nav items ---------- */
.ultra-navitem__count{margin-left:auto;font-size:var(--ultra-t-fine);font-family:var(--ultra-font-display);font-weight:var(--ultra-w-display);
  color:var(--ink-500);background:var(--cream-100);border-radius:999px;padding:1px 8px;flex:none}
.ultra-navitem__count--attention{color:var(--status-attention);background:rgba(var(--status-attention-rgb),0.26)}
.ultra-navitem__count--critical{color:var(--status-critical);background:rgba(var(--status-critical-rgb),0.22)}
.ultra-navitem__count--positive{color:var(--status-positive);background:rgba(var(--status-positive-rgb),0.22)}
.ultra-navitem[data-active="true"] .ultra-navitem__count{background:var(--blue-100);color:var(--blue-700)}
.ultra-shell[data-collapsed="true"] .ultra-navitem__count{display:none}

/* ---------- maps ----------
   Added for Ultra Nexus, which plots where an estate's traffic goes. Kept in
   the package rather than the application because "here is a place" is not a
   Nexus-only idea, and because a map drawn outside the system is exactly where
   a ninth colour gets invented. Every value below is an existing token: sea is
   the palest blue, land is the paper, and the marks are the one accent. */
.ultra-map{width:100%;background:var(--blue-050);border-radius:var(--ultra-r-sm);overflow:hidden;
  position:relative}
.ultra-map svg{display:block;width:100%;height:auto}
.ultra-map__cred{position:absolute;right:8px;bottom:6px;font-size:var(--ultra-t-label);color:var(--ink-400)}

/* ---------- dialog and notice ----------
   Added 2026-08-30 for the first Ultra application that writes. Every write
   surface needs the same two things and neither was in the package: somewhere
   to confirm and to say why, and somewhere to report what actually happened.
   Built out of the overlay the palette already uses, so there is one modal
   idiom rather than two. */
.ultra-dialog{position:fixed;inset:0;z-index:90;background:rgba(7,21,34,0.44);
  display:flex;justify-content:center;align-items:flex-start;padding-top:12vh}
.ultra-dialog[hidden]{display:none}
.ultra-dialog__box{width:min(520px,92vw);background:var(--white);border:1px solid var(--alpha-ink-14);
  border-radius:var(--ultra-r-panel);overflow:hidden;display:flex;flex-direction:column}
.ultra-dialog__box--form{width:min(760px,94vw)}
.ultra-dialog__head{padding:16px 18px 0;display:flex;flex-direction:column;gap:6px}
.ultra-dialog__title{font-family:var(--ultra-font-display);font-size:var(--ultra-t-head);font-weight:var(--ultra-w-display);
  letter-spacing:var(--ultra-track-display);line-height:var(--ultra-lh-head);color:var(--ink-900)}
.ultra-dialog__msg{font-size:var(--ultra-t-lede);color:var(--ink-500);line-height:var(--ultra-lh-text);max-width:var(--ultra-measure)}
/* Two columns where they fit, one where they do not, decided by the grid rather
   than by a breakpoint. A confirm with no fields keeps the narrow box; a dialog
   carrying a form gets the width to lay it out in, which is the whole reason
   not to send somebody to a page for it. */
/* Two columns rather than one field per row - a stack of full-width inputs
   makes a short form into a long page. A field that needs the room says so
   with --full; on a narrow drawer or phone the grid collapses to one column
   by itself, so nothing has to be re-authored per breakpoint. */
.ultra-dialog__body{padding:14px 18px;display:grid;
  grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:12px 16px;align-items:start}
.ultra-dialog__body .ultra-field--full{grid-column:1/-1}
.ultra-field{display:flex;flex-direction:column;gap:4px}
.ultra-field__label{font-size:var(--ultra-t-label);font-weight:var(--ultra-w-label);letter-spacing:var(--ultra-track-label);text-transform:uppercase;color:var(--ink-400)}
.ultra-field input,.ultra-field textarea,.ultra-field select{font-family:inherit;font-size:var(--ultra-t-lede);
  color:var(--ink-900);background:var(--white);border:1px solid var(--alpha-ink-14);
  border-radius:var(--ultra-r-chip);padding:7px 10px;outline:none;width:100%}
.ultra-field textarea{min-height:64px;resize:vertical;line-height:var(--ultra-lh-control)}
.ultra-field input:focus,.ultra-field textarea:focus,.ultra-field select:focus{border-color:var(--blue-500)}
.ultra-field__hint{font-size:var(--ultra-t-meta);color:var(--ink-400)}
.ultra-field__err{font-size:var(--ultra-t-meta);color:var(--status-critical)}
.ultra-dialog__foot{padding:12px 18px;border-top:1px solid var(--alpha-ink-10);
  display:flex;justify-content:flex-end;gap:10px}

/* One notice at a time, bottom right, dismissed by the next one or by time.
   Deliberately not a queue: a stack of toasts is a log nobody reads. */
.ultra-notice{position:fixed;right:20px;bottom:20px;z-index:95;max-width:min(460px,90vw);
  background:var(--white);border:1px solid var(--alpha-ink-14);border-radius:var(--ultra-r-card);
  padding:14px 18px;display:flex;gap:12px;align-items:flex-start;font-size:var(--ultra-t-body);
  color:var(--ink-800);line-height:var(--ultra-lh-text)}
.ultra-notice[hidden]{display:none}
.ultra-notice__dot{flex:none;margin-top:6px;width:8px;height:8px;border-radius:999px;background:var(--blue-500)}
.ultra-notice--positive .ultra-notice__dot{background:var(--status-positive)}
.ultra-notice--attention .ultra-notice__dot{background:var(--status-attention)}
.ultra-notice--critical .ultra-notice__dot{background:var(--status-critical)}
.ultra-notice__x{margin-left:auto;background:none;border:0;color:var(--ink-400);cursor:pointer;
  font:inherit;padding:0 0 0 8px}

/* Row actions inside a table. Small, right-aligned, and quiet until hovered:
   an action column that shouts competes with the data it acts on. */
.ultra-rowacts{display:flex;gap:4px;justify-content:flex-end;align-items:center;flex-wrap:wrap}
.ultra-rowbtn{font-family:inherit;font-size:var(--ultra-t-meta);line-height:var(--ultra-lh-control);color:var(--ink-500);background:none;
  border:1px solid var(--alpha-ink-14);border-radius:var(--ultra-r-chip);padding:2px 9px;
  cursor:pointer;white-space:nowrap;align-self:flex-start}
.ultra-rowbtn:hover{background:var(--cream-100);color:var(--ink-800)}
.ultra-rowbtn--danger:hover{background:rgba(var(--status-critical-rgb),0.14);color:var(--status-critical);
  border-color:rgba(var(--status-critical-rgb),0.40)}
.ultra-rowbtn[disabled]{opacity:.5;cursor:default}

/* ---------- drawer ----------
   The drill-in. A record opened from a table belongs beside the table it came
   from, not on a page of its own that loses the list. Right-hand panel, one at
   a time, closed by escape or by the scrim - the same rules as the dialog, so
   there is one way to be "on top of the page" and not three. */
.ultra-drawer{position:fixed;inset:0;z-index:85;display:flex;justify-content:flex-end}
.ultra-drawer[hidden]{display:none}
.ultra-drawer__scrim{position:absolute;inset:0;background:rgba(7,21,34,0.38)}
/* Wider than it was (620px). A drawer is where a record gets edited, and
   principle is that it must be comfortable enough that nobody needs a separate
   page for it - two form columns fit at this width, one did not. */
.ultra-drawer__box{position:relative;width:min(880px,94vw);height:100%;background:var(--white);
  border-left:1px solid var(--alpha-ink-14);display:flex;flex-direction:column}
/* Head and foot are flex siblings of a scrolling body, so both stay put while
   the record scrolls - the save action never scrolls out of reach. */
.ultra-drawer__head{padding:16px 20px 13px;border-bottom:1px solid var(--alpha-ink-10);
  display:flex;align-items:flex-start;gap:16px;flex:none}
.ultra-drawer__title{font-family:var(--ultra-font-display);font-size:var(--ultra-t-head);font-weight:var(--ultra-w-display);
  letter-spacing:var(--ultra-track-display);color:var(--ink-900);line-height:var(--ultra-lh-head)}
.ultra-drawer__sub{font-size:var(--ultra-t-meta);color:var(--ink-500);margin-top:5px}
.ultra-drawer__x{margin-left:auto;background:none;border:0;color:var(--ink-400);font-size:22px;
  line-height:1;cursor:pointer;padding:0 2px}
.ultra-drawer__x:hover{color:var(--ink-800)}
.ultra-drawer__body{flex:1;min-height:0;overflow-y:auto;padding:16px 20px;
  display:flex;flex-direction:column;gap:13px}
.ultra-drawer__foot{padding:12px 20px;border-top:1px solid var(--alpha-ink-10);
  display:flex;justify-content:flex-end;gap:10px;flex:none;background:var(--white)}
.ultra-drawer__foot:empty{display:none}

/* Label/value pairs, the drill-in's bread and butter. */
.ultra-kv{display:grid;grid-template-columns:150px 1fr;gap:10px 18px;font-size:var(--ultra-t-body);
  line-height:var(--ultra-lh-text);color:var(--ink-800)}
.ultra-kv dt{font-size:var(--ultra-t-label);font-weight:var(--ultra-w-label);letter-spacing:var(--ultra-track-label);text-transform:uppercase;color:var(--ink-400);
  padding-top:3px;margin:0}
.ultra-kv dd{margin:0;min-width:0;overflow-wrap:anywhere}
.ultra-prose{font-size:var(--ultra-t-body);line-height:var(--ultra-lh-text);color:var(--ink-700);max-width:var(--ultra-measure)}
.ultra-prose h4{margin:0 0 4px;font-size:var(--ultra-t-label);letter-spacing:var(--ultra-track-label);text-transform:uppercase;
  color:var(--ink-400);font-weight:var(--ultra-w-label)}
.ultra-prose + .ultra-prose{margin-top:2px}

/* A table row that can be opened. The cursor is the whole affordance; a chevron
   column would cost 40px on every table to say what hover already says. */
.ultra-table__row--open{cursor:pointer}
.ultra-table__row--open:hover{background:var(--cream-100)}

/* ---------- charts ----------
   Added 2026-08-30. Ultra Nexus plots throughput, load, latency and incident
   volume; a meter cannot say "and it spiked at 04:20". Kept in the package for
   the same reason as the maps: a chart drawn per-application is where a tenth
   colour and a second type scale get invented.

   Two series colours only - the accent and the navy - because those are the two
   grounds this system already has. A third is the pale blue. Anything needing
   four series is a table, not a chart. */
.ultra-chart{position:relative;width:100%;min-width:0}
.ultra-chart__plot{position:relative;width:100%;overflow:hidden}
.ultra-chart__plot svg{position:absolute;left:0;top:0}
/* Drawn at its real pixel size, so it is never scaled on one axis. */
.ultra-chart svg{display:block;overflow:visible}
.ultra-chart__grid{stroke:var(--alpha-ink-10);stroke-width:1;vector-effect:non-scaling-stroke}
.ultra-chart__axis{fill:var(--ink-400);font-size:var(--ultra-t-label);font-family:var(--ultra-font-body)}
.ultra-chart__area{fill:var(--blue-500);opacity:.10}
.ultra-chart__line{fill:none;stroke:var(--blue-500);stroke-width:1.75;stroke-linejoin:round;
  stroke-linecap:round;vector-effect:non-scaling-stroke}
.ultra-chart__line--b{stroke:var(--navy-600)}
.ultra-chart__area--b{fill:var(--navy-600)}
.ultra-chart__line--c{stroke:var(--blue-300)}
.ultra-chart__area--c{fill:var(--blue-300)}
.ultra-chart__col{fill:var(--blue-500)}
.ultra-chart__col--b{fill:var(--navy-600)}
.ultra-chart__col--c{fill:var(--blue-300)}
.ultra-chart__gap{fill:var(--status-critical-vivid);opacity:.55}
.ultra-chart__cursor{stroke:var(--ink-300);stroke-width:1;stroke-dasharray:2 3;
  vector-effect:non-scaling-stroke}
.ultra-chart__dot{fill:var(--white);stroke-width:2;vector-effect:non-scaling-stroke}
.ultra-chart__hit{fill:transparent;cursor:crosshair}
/* `hidden` is an HTML attribute and browsers do not reliably honour it inside
   SVG, so the idle cursor dots were painting at 0,0 - a stray mark in the top
   left of every chart. Say it in CSS, where SVG does obey. */
.ultra-chart__c[hidden]{display:none}

/* The readout sits above the plot rather than following the pointer: a tooltip
   that moves is a tooltip you chase, and on a touch screen it is nothing. */
.ultra-chart__read{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 16px;
  font-size:var(--ultra-t-meta);color:var(--ink-500);min-height:20px;margin-bottom:6px}
.ultra-chart__read b{color:var(--ink-900);font-family:var(--ultra-font-display);font-weight:var(--ultra-w-display)}
.ultra-chart__key{display:inline-flex;align-items:baseline;gap:6px}
.ultra-chart__swatch{width:9px;height:9px;border-radius:2px;background:var(--blue-500);
  display:inline-block;flex:none}
.ultra-chart__swatch--b{background:var(--navy-600)}
.ultra-chart__swatch--c{background:var(--blue-300)}
.ultra-chart__when{margin-left:auto;color:var(--ink-400)}

/* Heatmap: one hue, five steps. Intensity is the only variable, so the reader
   is comparing one thing and not decoding a rainbow. */
.ultra-heat{display:grid;gap:3px}
.ultra-heat__cell{border-radius:2px;background:var(--cream-100);aspect-ratio:1}
.ultra-heat__cell[data-l="1"]{background:var(--blue-100)}
.ultra-heat__cell[data-l="2"]{background:var(--blue-300)}
.ultra-heat__cell[data-l="3"]{background:var(--blue-500)}
.ultra-heat__cell[data-l="4"]{background:var(--blue-700)}
.ultra-heat__lab{font-size:var(--ultra-t-label);color:var(--ink-400);align-self:center}
.ultra-heat__legend{display:flex;align-items:center;gap:6px;font-size:var(--ultra-t-fine);color:var(--ink-400)}
.ultra-heat__legend i{width:11px;height:11px;border-radius:2px;display:inline-block}

/* A metric can carry a sparkline and a line of context under its label. The
   number says what it is now; the spark says whether that is unusual, which is
   most of what somebody glancing at a dashboard actually wants. */
.ultra-why{line-height:var(--ultra-lh-text)}
.ultra-metric__note{font-size:var(--ultra-t-fine);color:var(--ink-400);margin-top:-2px}
.ultra-metric__spark{margin-top:6px;height:22px}
.ultra-metric__spark svg{display:block;width:100%;height:22px}
.ultra-metric__spark .ultra-chart__line{stroke-width:1.6;opacity:.65}
.ultra-metric__spark .ultra-chart__area{opacity:.07}
.ultra-metric--accent .ultra-metric__note{color:var(--ink-400)}
.ultra-metric--accent .ultra-metric__spark .ultra-chart__line{stroke:var(--blue-500)}
.ultra-metric--accent .ultra-metric__spark .ultra-chart__area{fill:var(--blue-500)}

/* Commands, verbatim. A block of CLI is the one place in this system where the
   text must not be re-flowed, re-cased or prettified: it is going to be pasted
   into a device. */
.ultra-cli{margin:0;padding:14px 16px;background:var(--navy-900);color:var(--alpha-white-70);
  border-radius:var(--ultra-r-sm);font-family:var(--ultra-font-data);
  font-size:var(--ultra-t-meta);line-height:var(--ultra-lh-pre);white-space:pre;overflow-x:auto}

/* ---------- disclosure and reordering ----------
   A tree that can be collapsed, and rows that can be dragged into a new order
   or a new parent. Both are generic: any Ultra application with a hierarchy
   needs them, and drop feedback invented per-app is drop feedback that means
   something different on every screen.

   The indicator is a line, not a highlight. A highlighted row says "this row";
   a line between two rows says "here", which is the actual question when
   something is being placed. */
.ultra-disc{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;
  margin-right:6px;border:0;background:none;padding:0;color:var(--ink-400);cursor:pointer;flex:none}
.ultra-disc svg{transition:transform 140ms var(--ultra-ease)}
.ultra-disc[aria-expanded="true"] svg{transform:rotate(90deg)}
.ultra-disc:hover{color:var(--ink-800)}

/* The group bar reads the page's own horizontal padding rather than a 24px
   of its own, so its caret starts on the same left edge as the column head
   and the first name beneath it. */
.ultra-bar{display:flex;align-items:center;gap:12px;padding:11px var(--ultra-pad-x);
  background:var(--cream-100);border-bottom:1px solid var(--alpha-ink-10);
  font-size:var(--ultra-t-body);color:var(--ink-800);cursor:grab}
.ultra-bar:active{cursor:grabbing}
.ultra-bar__name{font-weight:var(--ultra-w-label)}
.ultra-bar__count{font-size:var(--ultra-t-fine);color:var(--ink-400)}
/* Without align-items the count stretched to the bar's full height and sat
   at the top of it while the button's label sat centred in its padding, so
   the two pieces of text on the right were never on the same line. */
.ultra-bar__acts{margin-left:auto;display:flex;align-items:center;gap:6px}

[data-drag]{cursor:grab}
[data-drag]:active{cursor:grabbing}
.ultra-dragging{opacity:.4}
/* The line sits inside the row's own box so it cannot be clipped by the card. */
.ultra-drop-before{box-shadow:inset 0 2px 0 0 var(--blue-500)}
.ultra-drop-after{box-shadow:inset 0 -2px 0 0 var(--blue-500)}
.ultra-drop-into{background:var(--blue-050);box-shadow:inset 0 0 0 2px var(--alpha-blue-35)}
.ultra-grip{color:var(--ink-300);flex:none;margin-right:2px}
.ultra-table__row:hover .ultra-grip{color:var(--ink-400)}

/* A row button used as a toggle needs a pressed state; without one a set of
   them is a row of buttons with no way to tell which is on. */
.ultra-rowbtn--on{background:var(--blue-050);border-color:var(--alpha-blue-35);color:var(--blue-700)}
.ultra-rowbtn--on:hover{background:var(--blue-100);color:var(--blue-700)}

/* A context menu. Right-click is the only place in a dense table where every
   action can be offered without a row of buttons competing with the data, so
   the shell owns one rather than each application inventing a popup. */
.ultra-menu{position:fixed;z-index:88;min-width:210px;background:var(--white);
  border:1px solid var(--alpha-ink-14);border-radius:var(--ultra-r-sm);padding:6px;
  display:flex;flex-direction:column;gap:1px}
.ultra-menu[hidden]{display:none}

/* A chart under the pointer. The same floating panel as the menu - a thing
   above the page keeps an edge - sized for a glance rather than a study: the
   windows that need choosing live in the drawer a click away, because a
   control inside something that follows the pointer is a control you have to
   chase. */
.ultra-pop{position:fixed;z-index:89;width:400px;background:var(--white);
  border:1px solid var(--alpha-ink-14);border-radius:var(--ultra-r-sm);padding:14px 16px;
  pointer-events:none}
.ultra-pop[hidden]{display:none}
.ultra-pop__name{font-size:var(--ultra-t-label);font-weight:var(--ultra-w-label);letter-spacing:var(--ultra-track-label);
  text-transform:uppercase;color:var(--ink-500)}
.ultra-pop__now{font-family:var(--ultra-font-data);font-variant-numeric:tabular-nums;
  font-size:var(--ultra-t-figure);color:var(--ink-900)}
/* A value that can be looked into says so on hover, in the way a link does:
   by changing, not by carrying a decoration the rest of the row does not. */
/* Something that explains itself on hover, marked the way a chartable figure
   is: by a dotted underline, not by a control the reader must open and then
   remember to close. */
.ultra-explain{cursor:help;border-bottom:1px dotted var(--alpha-ink-14)}
.ultra-explain:hover{color:var(--ink-900);border-bottom-color:currentColor}
.ultra-metric-link{cursor:pointer;border-bottom:1px dotted var(--alpha-ink-14)}
.ultra-metric-link:hover{color:var(--ink-900);border-bottom-color:currentColor}
.ultra-menu__head{font-size:var(--ultra-t-label);font-weight:var(--ultra-w-label);letter-spacing:var(--ultra-track-label);text-transform:uppercase;
  color:var(--ink-400);padding:6px 10px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ultra-menu__item{display:flex;align-items:center;gap:10px;padding:8px 10px;border:0;
  background:none;font-family:inherit;font-size:var(--ultra-t-body);color:var(--ink-800);text-align:left;
  border-radius:var(--ultra-r-chip);cursor:pointer;white-space:nowrap}
.ultra-menu__item:hover{background:var(--cream-100)}
.ultra-menu__item--danger:hover{background:rgba(var(--status-critical-rgb),0.14);color:var(--status-critical)}
.ultra-menu__sep{height:1px;background:var(--alpha-ink-10);margin:5px 2px}

/* ---------- two-column content ----------
   The metric row has always been a grid; the cards under it were a single
   stack, which on a 1440px screen left half the width empty and made every
   page twice as long as it needed to be. Two cards that answer related
   questions - applications and the devices using them - belong beside each
   other, where they can be read against one another.

   One column under 1180px: below that, two columns of table start truncating
   the very names they exist to show. */
.ultra-cols{display:grid;grid-template-columns:1fr 1fr;gap:20px;align-items:start}
.ultra-cols > *{min-width:0}
@media (max-width:1180px){
  .ultra-cols{grid-template-columns:1fr}
}

/* A meter carrying two directions rather than one total. Down is the accent,
   up is the navy - the same two series colours the charts use, so a bar and a
   line mean the same thing on the same page. */
.ultra-meter--split{display:flex;gap:2px;background:var(--cream-100)}
.ultra-meter--split .ultra-meter__fill{border-radius:0}
.ultra-meter__fill--up{background:var(--navy-600)}
.ultra-fig{display:flex;flex-direction:column;align-items:flex-end;gap:2px;
  font-size:var(--ultra-t-meta);line-height:var(--ultra-lh-tight);white-space:nowrap}
.ultra-fig b{font-family:var(--ultra-font-display);font-weight:var(--ultra-w-display);color:var(--ink-900)}
.ultra-fig span{color:var(--ink-500)}

/* ---------- categorical series ----------
   Four hues for encoding IDENTITY - which device, which link - as opposed to
   the single accent, which encodes emphasis. They are a different job and the
   one-accent rule does not cover them.

   Four and not more, and these four specifically, because the marks that use
   them overlap: ribbons in a flow diagram cross, so any pair can end up
   adjacent, and that is the validator's `--pairs all` case. Of the eight hues
   in the reference palette only these four clear it in both modes - worst
   normal-vision deltaE 19.6 light / 19.3 dark against a 15 floor. A fifth
   series folds into Other rather than inventing a hue.

   Dark is stepped for the dark surface, not flipped. Two light steps sit under
   3:1 on white and one dark pair is in the CVD warn band, so anything using
   them must carry a visible label - which is why every node in the flow is
   named and every band has a title. */
/* ---------- flow diagram ----------
   Hover is the whole interaction: one ribbon comes forward, the rest fall
   back, and the two nodes it joins light up with it. The transitions are
   .18s so it reads as a response rather than a flicker, and everything is
   opacity and colour - nothing moves, because a diagram that shifts under
   the pointer is hard to follow. */
.ultra-flow path{transition:fill-opacity .18s ease;fill-opacity:.38}
.ultra-flow__node rect,.ultra-flow__node text{transition:opacity .18s ease,fill .18s ease}
.ultra-flow__name{font-size:var(--ultra-t-meta)}
.ultra-flow .ultra-flow__name{fill:var(--ink-800)}
.ultra-flow[data-hot] path{fill-opacity:.07}
.ultra-flow[data-hot] path.is-hot{fill-opacity:.92}
.ultra-flow[data-hot] .ultra-flow__node{opacity:.30}
.ultra-flow[data-hot] .ultra-flow__node.is-hot{opacity:1}
.ultra-flow[data-hot] .ultra-flow__node.is-hot .ultra-flow__name{fill:var(--ink-900)}
@media (prefers-reduced-motion:reduce){
  .ultra-flow path,.ultra-flow__node rect,.ultra-flow__node text{transition:none}}

/* The topology map. Same hover contract as the flow diagram - data-hot on the
   svg, is-hot on what should stay lit - so the two diagrams behave identically
   and there is one idea to learn rather than two.
   The link styling carries the argument: a CONFIRMED link is solid in the
   accent, an inferred one is a faint dash. Without SNMP, LLDP or a forwarding
   table there is no evidence of what is plugged into what, and drawing a guess
   at full strength would invent a topology that nobody measured. */
/* The bare map: the window given to the diagram, and a single thin strip for
   the controls. It sits outside <ultra-shell> so nothing here has to out-rank
   the chrome - there is no chrome. */
.ultra-bare{position:fixed;inset:0;z-index:1;display:flex;flex-direction:column;
  background:var(--cream-50);color:var(--ink-800);font-family:var(--ultra-font-body)}
.ultra-bare[hidden]{display:none}
.ultra-bare__strip{flex:none;display:flex;align-items:center;gap:16px;
  padding:10px 16px;border-bottom:1px solid var(--ultra-rule)}
.ultra-bare__find{width:min(340px,34vw);flex:none}
.ultra-bare__acts{margin-left:auto;display:flex;align-items:center;gap:4px}
.ultra-bare__stage{flex:1;min-height:0;overflow:hidden;position:relative}
.ultra-bare__stage svg{width:100%;height:100%;display:block;cursor:grab;touch-action:none}
.ultra-bare__stage svg.is-dragging{cursor:grabbing}
.ultra-bare__note{padding:24px;color:var(--ink-500)}

.ultra-topo{display:block;max-width:100%;overflow:visible}
.ultra-topo__link{fill:none;stroke:var(--ultra-rule);stroke-width:1;
  stroke-dasharray:3 4;transition:stroke-opacity .18s ease,stroke .18s ease}
.ultra-topo__link.is-confirmed{stroke:var(--blue-500);stroke-width:1.6;stroke-dasharray:none}
/* The cluster ring is a hairline, not a card: it groups without becoming an
   object of its own. A measured group - stations the firewall placed on an
   access point - takes the accent; an unmeasured one stays a quiet dash. */
.ultra-topo__ring{fill:none;stroke:var(--ultra-rule);stroke-width:1;
  stroke-dasharray:4 4;transition:stroke .18s ease,opacity .18s ease}
.ultra-topo__group.is-measured .ultra-topo__ring{stroke:var(--alpha-blue-35);stroke-dasharray:none}
.ultra-topo__label{font-size:var(--ultra-t-label);font-family:var(--ultra-font-label);
  letter-spacing:var(--ultra-track-label);fill:var(--ink-400)}
/* A filled badge with the glyph on it, the shape the original used, because at
   tile size the fill is what carries the colour. The glyph is stroked in the
   page's own ground so it inverts with the theme rather than being white in
   both - white on the light palette's deep teal is right, white on the dark
   palette's bright teal is not. */
.ultra-topo__badge{fill:var(--ink-300);stroke:var(--cream-50);stroke-width:2}
.ultra-topo__badge--s1{fill:var(--series-1)}
.ultra-topo__badge--s2{fill:var(--series-2)}
.ultra-topo__badge--s3{fill:var(--series-3)}
.ultra-topo__badge--s4{fill:var(--series-4)}
.ultra-topo__glyph{fill:none;stroke:var(--cream-50)}
.ultra-topo__you{font-size:var(--ultra-t-label);font-family:var(--ultra-font-label);
  letter-spacing:var(--ultra-track-label);fill:var(--blue-500)}
.ultra-topo__live{fill:var(--status-positive);stroke:var(--cream-50);stroke-width:1.5}
.ultra-topo__live.is-off{fill:var(--ink-300)}
.ultra-topo__node.is-offline{opacity:.45}
/* Arrived in the last quarter of an hour. A ring rather than another colour:
   the badge colour already means what kind of device it is, and "new" is a
   different question from "what". It pulses, gently, because an arrival is the
   one thing on this map worth catching an eye that is looking elsewhere. */
.ultra-topo__node.is-new .ultra-topo__badge{stroke:var(--blue-500);stroke-width:2.5}
.ultra-topo__node.is-new .ultra-topo__live{animation:ultra-topo-new 2.4s ease-out infinite}
@keyframes ultra-topo-new{
  0%{r:3.6;opacity:1} 70%{r:7;opacity:0} 100%{r:7;opacity:0}}
@media (prefers-reduced-motion:reduce){
  .ultra-topo__node.is-new .ultra-topo__live{animation:none}}
.ultra-topo__name{font-size:var(--ultra-t-fine);font-family:var(--ultra-font-body);
  fill:var(--ink-800)}
.ultra-topo__addr{font-size:var(--ultra-t-label);font-family:var(--ultra-font-data);
  fill:var(--ink-400)}
.ultra-topo__node,.ultra-topo__group{transition:opacity .18s ease}
.ultra-topo__node[onclick]{cursor:pointer}
/* Choosing a kind from the filter does not remove anything from the map: it
   lights where those devices are. Removing them would answer "how many" and
   throw away "where", which is the only question a map is better at. */
.ultra-topo[data-pick] .ultra-topo__node{opacity:.32}
.ultra-topo[data-pick] .ultra-topo__node.is-pick{opacity:1}
.ultra-topo[data-pick] .ultra-topo__group{opacity:.55}
.ultra-topo[data-hot] .ultra-topo__link{stroke-opacity:.12}
.ultra-topo[data-hot] .ultra-topo__link.is-hot{stroke-opacity:1;stroke:var(--blue-500)}
.ultra-topo[data-hot] .ultra-topo__group{opacity:.26}
.ultra-topo[data-hot] .ultra-topo__group.is-hot{opacity:1}
.ultra-topo[data-hot] .ultra-topo__node{opacity:.34}
.ultra-topo[data-hot] .ultra-topo__node.is-hot{opacity:1}
.ultra-topo[data-hot] .ultra-topo__group.is-hot .ultra-topo__node{opacity:1}
.ultra-topo[data-hot] .ultra-topo__node.is-hot .ultra-topo__name{fill:var(--ink-900)}
@media (prefers-reduced-motion:reduce){
  .ultra-topo__link,.ultra-topo__ring,.ultra-topo__node,.ultra-topo__group{transition:none}}

.ultra-legend{display:flex;flex-wrap:wrap;gap:6px 16px;align-items:center;
  padding:0 24px 14px;font-size:var(--ultra-t-meta);color:var(--ink-500)}
.ultra-legend i{width:10px;height:10px;border-radius:2px;display:inline-block;margin-right:6px}

/* Eyebrow removed 2026-08-30 at the user's request, across every page of every
   application. Done here rather than in each app because it is authored in
   dozens of files across seven repositories - and because the look of a page is
   the package's job. Delete this rule to bring it back. */
.ultra-eyebrow{display:none}

/* ================================================================
   CELL VOCABULARY                                     2026-09-09
   ================================================================
   Every cell in every table is one of eight kinds, and the kind decides
   the face, the weight, the colour and the alignment - a page decides
   none of those. Before this, a row mixed three faces (a bold display
   name, a bold display IP, a sans MAC) and each page chose its own
   greys and its own right-alignment, which is why columns looked
   designed independently: they were.

     name    the thing the row is about. Body face at 500, ink-900.
             May carry a second line (.ultra-cell__sub) at the meta step.
     addr    an IP, MAC or hostname-as-address. Data face, tabular.
     num     a quantity: bytes, counts, Mbps, %. Data face, 500, right.
     time    a duration or timestamp. Data face, quiet, right.
     text    descriptive words: interface, direction, category. Quiet.
     status  a badge. Left, never wraps.
     acts    row buttons. Right.
     check   a checkbox or grip. Fixed and centred.

   The head cell for a column carries the SAME class as its body cells,
   so a column's head aligns with its values by construction; the reset
   below stops the data face and colour leaking into the head. */
/* A device is one line. A name long enough to wrap used to make its row two
   or three rows tall and break the rhythm of every row around it, so it
   truncates instead; the whole name is in the row's own drawer. */
.ultra-cell--name{font-weight:var(--ultra-w-name);color:var(--ink-900);min-width:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* The second line under a name is what the row IS technically - an address,
   a MAC, a byte total, a machine-reported label - so it takes the data face
   like every other figure rather than the body face the name above it uses. */
.ultra-cell__sub{display:block;font-family:var(--ultra-font-data);
  font-size:var(--ultra-t-meta);font-weight:400;color:var(--ink-500);
  font-variant-numeric:tabular-nums;
  line-height:var(--ultra-row-line);margin-top:1px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ultra-cell--addr{font-family:var(--ultra-font-data);font-variant-numeric:tabular-nums;color:var(--ink-700);
  white-space:nowrap}
.ultra-cell--num{font-family:var(--ultra-font-data);font-variant-numeric:tabular-nums;font-weight:var(--ultra-w-data);
  color:var(--ink-900);text-align:right;white-space:nowrap}
/* Clipped, not spilling. nowrap with no overflow rule meant a cell wider than
   its column ran straight over the next one: a full "15/09/2026, 09:02:37" in
   a 132px time column printed itself across the SEVERITY badge beside it. Every
   other cell kind already truncates; this one only ever held short ago()
   strings until something put a date in it. */
.ultra-cell--time{font-family:var(--ultra-font-data);font-variant-numeric:tabular-nums;color:var(--ink-500);
  text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
/* One line, like every other kind. This was the only cell that could wrap,
   which is what made rows two and three deep and broke the rhythm of the rows
   around them - the same complaint on the device table and on Renewals. The
   whole value is in the row's own drawer; the column shows what fits. */
.ultra-cell--text{color:var(--ink-500);min-width:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* A reading rather than a word: "0% loss · 1.3 ms". Left like text because
   it sits beside names, but in the data face like every other figure - it
   was the one column putting numbers in the body face, which is what made
   a row look like it used a different font in every column. */
.ultra-cell--meas,.ultra-cell--measwide{font-family:var(--ultra-font-data);font-variant-numeric:tabular-nums;
  color:var(--ink-500);min-width:0;overflow:hidden}
.ultra-cell--meas{white-space:nowrap;text-overflow:ellipsis}
/* The one cell allowed to run to a second line. A device somebody has put ten
   readings on cannot say them in the width of an address column, and clipping
   them hides a figure that was deliberately chosen - which is the opposite of
   choosing it. Two lines, never three: the row grows only when it has to, so
   the thirty-odd devices watched for reachability alone stay one line each and
   the table stays scannable. The count that can be chosen is budgeted against
   this width in the picker, so the ceiling is enforced where the choice is
   made rather than by silently cutting the result. */
.ultra-cell--measwide{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;
  -webkit-line-clamp:2;line-clamp:2}
.ultra-cell--status{white-space:nowrap}
/* Clipped, not overlapping: a flex row justified to the end that outgrows its
   box spills to the LEFT, which drew a row's buttons across the status column
   next to it. If a table's buttons need more room it says so with --acts. */
.ultra-cell--acts{text-align:right;overflow:hidden}
.ultra-cell--check{display:flex;align-items:center;justify-content:center;gap:6px}
.ultra-cell--cell{font-family:var(--ultra-font-data);font-variant-numeric:tabular-nums;text-align:right;
  white-space:nowrap;min-width:0}
/* min-width:0 on every cell, so a long value can never widen its own track
   and pull that row out of line with the heading above it. Cells that must
   not wrap say so themselves; the rest wrap inside the width they are given. */
.ultra-table__head > *,.ultra-table__row > *{min-width:0}
.ultra-table__head > div{font-family:inherit;font-weight:inherit;color:inherit;font-variant-numeric:normal;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* ---------- layout roles ----------
   The three arrangements a view actually needs, so it stops writing
   display:flex;gap:10px;align-items:center by hand 26 different ways. */
.ultra-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;min-width:0}
.ultra-row--between{justify-content:space-between}
.ultra-row--base{align-items:baseline}
.ultra-row--top{align-items:flex-start}
.ultra-stack{display:flex;flex-direction:column;gap:var(--ultra-gap-card);min-width:0}
.ultra-stack--tight{gap:4px}
.ultra-inset{padding:var(--ultra-pad-card)}
.ultra-inset--x{padding:0 var(--ultra-pad-x)}
.ultra-inset--row{padding:var(--ultra-pad-row)}

/* ---------- modules in the column ----------
   Modules sit at the top of the sidebar as a first group, above the pages
   of whichever module is open, rather than behind a switcher in the top
   bar. The active module is marked by weight and an accent glyph, not by a
   fill, so it never competes with the active page beneath it. */
.ultra-modnav{display:flex;flex-direction:column;gap:2px;flex:none;
  padding-bottom:10px;margin-bottom:10px;border-bottom:1px solid var(--alpha-ink-10)}
.ultra-modnav .ultra-navitem{font-weight:var(--ultra-w-name)}
.ultra-modnav .ultra-navitem[data-active="true"]{background:transparent;color:var(--ink-900)}
.ultra-modnav .ultra-navitem[data-active="true"] svg{color:var(--blue-600)}
.ultra-shell[data-collapsed="true"] .ultra-modnav{padding-bottom:8px;margin-bottom:8px}

/* ---------- rail chrome ----------
   chrome="rail" on <ultra-shell>: no top bar. The column runs the full
   height, search sits at its top and the theme toggle and the person at
   its foot; the page carries its own top line (.ultra-topline) - what the
   screen is about on the left, product · state · time on the right. */
.ultra-shell[chrome="rail"] .ultra-body{height:100vh}
/* Ctrl+K, centred, gone on Escape. */
.ultra-palette{position:fixed;inset:0;z-index:88;display:flex;justify-content:center;
  align-items:flex-start;padding-top:14vh;background:rgba(7,21,34,0.38)}
.ultra-palette .ultra-searchwrap{width:min(620px,92vw);max-width:none;flex:none}
.ultra-palette .ultra-search{max-width:none;width:100%;padding:12px 14px}
.ultra-palette .ultra-results{width:100%;min-width:0}
/* An author `display` beats the UA's [hidden], so every overlay here has to
   say this for itself - the same line the drawer, dialog, menu and notice
   each carry a few rules up. */
.ultra-palette[hidden]{display:none}
.ultra-railfoot{display:flex;align-items:center;gap:8px;padding:2px 10px 8px;color:var(--ink-500);
  font-size:var(--ultra-t-body)}
/* .ultra-user is white because it was only ever drawn on the navy top bar.
   In the rail it sits on the sidebar, which is white in the light theme, so
   the person's name was white on white - invisible, and only in that one
   combination of chrome and theme. */
.ultra-railfoot .ultra-user{display:flex;align-items:center;gap:8px;min-width:0;color:inherit}
.ultra-railfoot .ultra-user span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ultra-railfoot .ultra-iconbtn{color:var(--ink-400)}

.ultra-shell[data-collapsed="true"] .ultra-railfoot{flex-direction:column;padding:2px 0 8px}
.ultra-shell[data-collapsed="true"] .ultra-railfoot .ultra-user span{display:none}
.ultra-topline{display:none;justify-content:space-between;gap:16px;align-items:baseline;
  margin-bottom:6px}
.ultra-shell[chrome="rail"] .ultra-topline{display:flex}

/* ================================================================
   THE DARK PALETTE                                     2026-09-09
   ================================================================
   Structure lives in the base rules; a theme supplies colour and
   nothing else. This is the world the design was drawn for: a deep
   blue-black ground, one cyan accent, and status at values that
   measure 6-11:1 on it. */
/* Also on the root, so a screen drawn OUTSIDE the shell is coloured too. The
   bare map is a sibling of <ultra-shell> on purpose - see .ultra-bare - and a
   palette scoped to the shell alone left it permanently light whatever the
   toggle said. index.html mirrors the shell's stamp onto <html>. */
.ultra-shell[data-theme="dark"],
:root[data-theme="dark"]{
  --cream-50:#05090c; --cream-100:#0d161b; --white:#0a1217;
  --ink-900:#f2fbfc; --ink-800:#dcecef; --ink-700:#c3d9de;
  --ink-500:#93b3ba; --ink-400:#7595a0; --ink-300:#55707a; --ink-100:#16232a;
  --navy-900:transparent; --navy-850:rgba(10,22,28,.55);
  --alpha-ink-10:rgba(140,200,210,.10); --alpha-ink-14:rgba(140,200,210,.16);
  --blue-050:rgba(62,224,200,.07); --blue-100:rgba(62,224,200,.12);
  --blue-500:#3ee0c8; --blue-600:#5ff0da; --blue-700:#8ff7e6; --blue-300:#9fdfe2;
  --alpha-blue-35:rgba(62,224,200,.35);
  --status-positive:#2ee6bd; --status-attention:#ffc14d; --status-critical:#ff8f7e;
  --status-positive-vivid:#2ee6bd; --status-attention-vivid:#ffc14d; --status-critical-vivid:#ff8f7e;
  --status-positive-rgb:46,230,189; --status-attention-rgb:255,193,77; --status-critical-rgb:255,143,126;
  --series-1:#3ee0c8; --series-2:#4f9ae8; --series-3:#9b8cf0; --series-4:#48c98d;
  --series-other:#3f5a66;
  /* The surfaces that are not flat colour: the light behind the glass, the
     grid over it, the panels that float above the page, and the tint a row
     takes when it is hovered or marked. Named so the light theme can answer
     the same questions with its own values rather than reimplement them. */
  --ultra-rule:rgba(140,200,210,.17); --ultra-rule-soft:rgba(140,200,210,.08);
  --ultra-rule-strong:rgba(140,200,210,.22);
  --ultra-glass:rgba(8,18,24,.86); --ultra-scrim:rgba(2,6,9,.62);
  --ultra-chrome:rgba(5,12,16,.42); --ultra-chrome-side:rgba(5,12,16,.30);
  --ultra-panel:rgba(10,26,34,.45);
  --ultra-tint:rgba(62,224,200,.05);
  --ultra-aurora-a:rgba(30,150,160,.30); --ultra-aurora-b:rgba(24,72,132,.36);
  --ultra-aurora-c:rgba(90,60,150,.16); --ultra-grid-line:rgba(120,190,200,.04);
  --ultra-grid-opacity:.5;
  --ultra-avatar:rgba(62,224,200,.18);
}

/* ================================================================
   THE LIGHT PALETTE                                    2026-09-09
   ================================================================
   The same design in daylight, not the old boxed interface repainted.
   A cool near-white ground rather than pure white, so the hairlines
   and the glass have something to sit on; the accent darkens to a
   teal that carries 4.5:1 as text; status darkens for the same
   reason. Everything structural is shared, so this file cannot drift
   into two designs again - there is only one, twice coloured. */
:root{
  --cream-50:#f6f8f9; --cream-100:#eef2f4; --white:#ffffff;
  --ink-900:#0a1418; --ink-800:#16262c; --ink-700:#2c3a40;
  --ink-500:#566970; --ink-400:#5e7077; --ink-300:#7c8d94; --ink-100:#e8eef0;
  --navy-900:transparent; --navy-850:rgba(240,245,247,.72);
  --alpha-ink-10:rgba(16,54,64,.12); --alpha-ink-14:rgba(16,54,64,.18);
  --blue-050:rgba(13,124,110,.06); --blue-100:rgba(13,124,110,.11);
  --blue-500:#0d7c6e; --blue-600:#0a6659; --blue-700:#085449; --blue-300:#3f9c8f;
  --alpha-blue-35:rgba(13,124,110,.35);
  --status-positive:#0a7a5f; --status-attention:#8a5a00; --status-critical:#b3261e;
  --status-positive-vivid:#0a7a5f; --status-attention-vivid:#8a5a00; --status-critical-vivid:#b3261e;
  --status-positive-rgb:10,122,95; --status-attention-rgb:138,90,0; --status-critical-rgb:179,38,30;
  --series-1:#0d7c6e; --series-2:#2f6fb5; --series-3:#6a5bc2; --series-4:#2f8a5c;
  --series-other:#8b9aa1;
  --ultra-rule:rgba(16,54,64,.14); --ultra-rule-soft:rgba(16,54,64,.07);
  --ultra-rule-strong:rgba(16,54,64,.20);
  --ultra-glass:rgba(255,255,255,.92); --ultra-scrim:rgba(18,32,38,.34);
  --ultra-chrome:rgba(246,248,249,.72); --ultra-chrome-side:rgba(246,248,249,.55);
  --ultra-panel:rgba(255,255,255,.62);
  --ultra-tint:rgba(13,124,110,.05);
  /* The light behind the glass is a suggestion here rather than a display:
     the same three sources, at a fraction of the strength, because on a pale
     ground anything more reads as a stain rather than as depth. */
  --ultra-aurora-a:rgba(13,124,110,.10); --ultra-aurora-b:rgba(47,111,181,.09);
  --ultra-aurora-c:rgba(106,91,194,.06); --ultra-grid-line:rgba(16,54,64,.020);
  --ultra-grid-opacity:.55;
  --ultra-avatar:rgba(13,124,110,.14);
}

/* Shared by both: the shape of the type and the rhythm of the page. */
:root{
  --ultra-pad-card:14px 0; --ultra-pad-cardhead:11px 0;
  --ultra-pad-row:7px 0;   --ultra-pad-thead:7px 0; --ultra-pad-x:0px;
  --ultra-w-display:300; --ultra-w-label:500; --ultra-w-data:400;
  --ultra-track-display:-.035em; --ultra-track-label:.20em;
  --ultra-font-data:"IBM Plex Mono",ui-monospace,monospace;
  --ultra-font-label:"IBM Plex Mono",ui-monospace,monospace;
}


/* The light is behind the glass, not on the panels: a slow aurora under a
   56px grid. Both sit beneath the chrome and take no pointer events. */
.ultra-shell::before{
  content:"";position:absolute;inset:-25%;pointer-events:none;z-index:0;
  background:
    radial-gradient(46% 40% at 22% 26%, var(--ultra-aurora-a), transparent 66%),
    radial-gradient(38% 46% at 76% 68%, var(--ultra-aurora-b), transparent 68%),
    radial-gradient(30% 30% at 58% 10%, var(--ultra-aurora-c), transparent 70%);
  filter:blur(30px)}
.ultra-shell::after{
  content:"";position:absolute;inset:0;pointer-events:none;z-index:0;opacity:var(--ultra-grid-opacity);
  background-image:linear-gradient(var(--ultra-grid-line) 1px,transparent 1px),
                   linear-gradient(90deg,var(--ultra-grid-line) 1px,transparent 1px);
  background-size:56px 56px}
/* Only the two children that sit IN the page get lifted above the aurora.
   `> *` also caught the drawer, dialog, menu and notice - and at (0,2,0) it
   beat their own `position:fixed` (0,1,0), so every overlay quietly became a
   block in normal flow at the bottom of the document. That is what put the
   Settings drawer below the table with a band of bare page beside it. */
.ultra-shell > .ultra-topbar,
.ultra-shell > .ultra-body{position:relative;z-index:1}

/* Chrome becomes glass rather than a filled bar. */
.ultra-shell .ultra-topbar{
  background:var(--ultra-chrome);backdrop-filter:blur(14px);
  border-bottom:1px solid var(--ultra-rule-soft)}
.ultra-shell .ultra-sidebar{
  background:var(--ultra-chrome-side);backdrop-filter:blur(10px);
  border-right:1px solid var(--ultra-rule-soft)}
/* A field is a hairline under the text, never a box around it. */
.ultra-shell .ultra-search{
  background:transparent;border:0;border-radius:0;
  border-bottom:1px solid var(--ultra-rule-strong)}
.ultra-shell .ultra-search__key{border:0;background:transparent}
.ultra-shell .ultra-switcher{
  background:transparent;border:0;border-radius:0}

/* No boxes. Cards, tiles and panels give up fill and border; a card keeps a
   single hairline on top so a stack still reads as separate things. */
.ultra-shell .ultra-card,
.ultra-shell .ultra-metric{
  background:transparent;border:0;border-radius:0;
  border-top:1px solid var(--ultra-rule)}
.ultra-shell .ultra-card__head{
  border-bottom:1px solid var(--ultra-rule-soft)}
/* The emphasis panel keeps its glass but not its inset: its own 22px of
   padding was the last thing on the page starting somewhere of its own,
   with the WAN link rows above it and Firewall memory below it both on the
   page edge. */
.ultra-shell .ultra-panel-navy{
  background:var(--ultra-panel);backdrop-filter:blur(10px);border:0;border-radius:0;
  padding-left:0;padding-right:0;color:var(--ink-700)}
/* The panel's own colours are white, from when it was a navy fill. It is a
   tinted glass block now, and in the light palette that block is pale - white
   on near-white, unreadable, and check-contrast.py does not look at it because
   it is not one of the text roles. Its ink comes from the same tokens as the
   rest of the page, in both themes. Same for the ghost button that sits in it. */
.ultra-shell .ultra-panel-navy__title{color:var(--ink-900)}
.ultra-shell .ultra-panel-navy__meta{color:var(--ink-500)}
.ultra-shell .ultra-btn--ghost-dark{color:var(--ink-700);border-color:var(--ultra-rule)}
.ultra-shell .ultra-btn--ghost-dark:hover{background:var(--ultra-tint);color:var(--ink-900)}

/* The metric label reads as a caption under its figure, so it takes the
   uppercase treatment the label role implies rather than sentence case. The
   face itself comes from --ultra-font-label, set once above. */
.ultra-shell .ultra-metric__label,
.ultra-shell .ultra-card__link{
  font-family:var(--ultra-font-label);font-size:var(--ultra-t-label);
  letter-spacing:var(--ultra-track-label);text-transform:uppercase;color:var(--ink-400)}
.ultra-shell .ultra-modnav{border-bottom-color:var(--ultra-rule)}
.ultra-shell .ultra-modnav .ultra-navitem[data-active="true"]{
  box-shadow:none;color:var(--blue-700)}
.ultra-shell .ultra-modnav .ultra-navitem[data-active="true"] svg{color:var(--blue-500)}

/* The table head stops being a navy bar and becomes a ruled caption. */
.ultra-shell .ultra-table__head{
  background:transparent;color:var(--ink-400);
  border-bottom:1px solid var(--ultra-rule-strong)}
.ultra-shell .ultra-table__row{
  border-bottom:1px solid var(--ultra-rule-soft)}
.ultra-shell .ultra-table__row--open:hover,
.ultra-shell .ultra-table__row--marked{
  background:var(--ultra-tint)}

/* Status is coloured text. Not a pill, not an outline - Jag, 2026-09-09:
   "there shouldn't be any boxes anymore across the whole system." The
   colour and the tracked uppercase are the whole signal. */
.ultra-shell .ultra-badge{
  background:transparent;border:0;border-radius:0;padding:0;color:var(--ink-500)}
.ultra-shell .ultra-badge--positive{color:var(--status-positive)}
.ultra-shell .ultra-badge--attention{color:var(--status-attention)}
.ultra-shell .ultra-badge--critical{color:var(--status-critical)}
.ultra-shell .ultra-badge--info,
.ultra-shell .ultra-badge--live{color:var(--blue-500);background:transparent}
.ultra-shell .ultra-navitem__count{background:transparent;padding:0}

/* Buttons are text. The primary is the accent, danger is the critical
   colour, and hover brightens - no fill, no edge. The click target keeps
   its padding; only the box goes. */
.ultra-shell .ultra-btn,
.ultra-shell a.ultra-btn{
  background:transparent;border:0;border-radius:0;color:var(--ink-500);
  font-family:var(--ultra-font-label);font-size:var(--ultra-t-label);font-weight:var(--ultra-w-label);
  letter-spacing:var(--ultra-track-label);text-transform:uppercase}
.ultra-shell .ultra-btn:hover,
.ultra-shell a.ultra-btn:hover{background:transparent;color:var(--ink-900)}
.ultra-shell .ultra-btn--primary,
.ultra-shell a.ultra-btn--primary{background:transparent;color:var(--blue-500)}
.ultra-shell .ultra-btn--primary:hover,
.ultra-shell a.ultra-btn--primary:hover{color:var(--blue-700)}
.ultra-shell .ultra-btn--danger,
.ultra-shell a.ultra-btn--danger{background:transparent;color:var(--status-critical)}
.ultra-shell .ultra-rowbtn{
  background:transparent;border:0;color:var(--ink-400);
  font-family:var(--ultra-font-label);letter-spacing:var(--ultra-track-label);text-transform:uppercase;
  font-size:var(--ultra-t-label)}
.ultra-shell .ultra-rowbtn:hover{background:transparent;color:var(--blue-700)}
.ultra-shell .ultra-rowbtn--danger{color:var(--status-critical)}
.ultra-shell .ultra-iconbtn{background:transparent;border:0}

/* Overlays are glass over the aurora rather than solid sheets. */
.ultra-shell .ultra-drawer__box,
.ultra-shell .ultra-dialog__box,
.ultra-shell .ultra-menu,
.ultra-shell .ultra-notice{
  background:var(--ultra-glass);backdrop-filter:blur(22px);
  border-color:var(--ultra-rule)}
.ultra-shell .ultra-drawer__scrim{background:var(--ultra-scrim)}
.ultra-shell .ultra-palette .ultra-search,
.ultra-shell .ultra-field input,
.ultra-shell .ultra-field textarea,
.ultra-shell .ultra-field select{
  background:transparent;border:0;border-radius:0;
  border-bottom:1px solid var(--ultra-rule-strong);color:var(--ink-900)}
.ultra-shell .ultra-field input:focus,
.ultra-shell .ultra-field textarea:focus,
.ultra-shell .ultra-palette .ultra-search:focus-within{
  outline:none;border-bottom-color:var(--blue-500)}
.ultra-shell .ultra-railfoot{color:var(--ink-400)}
.ultra-shell .ultra-avatar{background:var(--ultra-avatar);color:var(--blue-700)}

/* Heat without a fill. The package's ramp is the accent, and here the
   accent is cyan, so a filled cell put cyan text on cyan. Two readings,
   chosen per table with data-heat on .ultra-table:
     text  the value alone carries the magnitude - quieter grey for less,
           ink for more, the accent for the most. Nothing drawn.
     bar   the value stays ink and a 2px accent bar under it grows with
           the magnitude, the same mark as the severity share bars. */
.ultra-shell .ultra-heat__cell{
  background:transparent;border-radius:0;color:var(--ink-700)!important;
  display:inline-block;position:relative;padding-bottom:3px;font-variant-numeric:tabular-nums}
.ultra-shell [data-heat="text"] .ultra-heat__cell[data-l="1"]{color:var(--ink-400)!important}
.ultra-shell [data-heat="text"] .ultra-heat__cell[data-l="2"]{color:var(--ink-700)!important}
.ultra-shell [data-heat="text"] .ultra-heat__cell[data-l="3"]{color:var(--ink-900)!important}
.ultra-shell [data-heat="text"] .ultra-heat__cell[data-l="4"]{color:var(--blue-500)!important;font-weight:var(--ultra-w-data)}
.ultra-shell [data-heat="bar"] .ultra-heat__cell{color:var(--ink-900)!important}
.ultra-shell [data-heat="bar"] .ultra-heat__cell::after{
  content:"";position:absolute;left:0;bottom:0;height:2px;background:var(--blue-500);opacity:.85}
.ultra-shell [data-heat="bar"] .ultra-heat__cell[data-l="1"]::after{width:25%;opacity:.45}
.ultra-shell [data-heat="bar"] .ultra-heat__cell[data-l="2"]::after{width:50%;opacity:.6}
.ultra-shell [data-heat="bar"] .ultra-heat__cell[data-l="3"]::after{width:75%}
.ultra-shell [data-heat="bar"] .ultra-heat__cell[data-l="4"]::after{width:100%}

/* Nav: the active item is a lit edge, not a filled block. */
.ultra-shell .ultra-navitem[data-active="true"]{
  background:transparent;color:var(--blue-700);box-shadow:inset 2px 0 0 var(--blue-500)}
.ultra-shell .ultra-navitem:hover{background:var(--ultra-tint)}
.ultra-shell .ultra-navitem svg{stroke-width:1.2;color:var(--ink-400)}
.ultra-shell .ultra-navgroup__label,
.ultra-shell .ultra-build{color:var(--ink-300)}
