/* RFSA-110: the Fleet Dashboard's two type families, served from its own static assets.
   A grotesque (Archivo) for UI text and a monospace (IBM Plex Mono) for every machine-generated
   value — timestamps, ages, UNC paths, credentials, setting keys, file names, counts.

   Self-hosted rather than pulled from a font CDN, which is what the RFSA-109 design canvas does:
   this is the page an operator opens to diagnose an outage, and it must not depend on a third party
   being reachable to render the readings they are opening it for. It is also public-internet-facing,
   so every request it makes on an operator's behalf is one more party that learns they are looking.

   Provenance, the exact commands that produced these files, and why only these subsets and weights
   ship are in wwwroot/fonts/README.md. Both families are SIL OFL; each licence ships beside the
   files it covers, as archivo-LICENSE.txt and ibm-plex-mono-LICENSE.txt.

   Static per-weight instances rather than one variable font per family: every weight the pages
   compute is a real file, so no weight is ever synthesised — a browser asked for a weight it has no
   file for smears the nearest one, which reads as a heavier font rather than as a fault. Archivo
   ships four (400 for body copy, 500 and 600 where a rule says so, 700 for h1/h2/th and the
   <strong> in the credential panels, since body's family inherits everywhere); IBM Plex Mono ships
   one, because every value it is for sits in a cell that inherits normal weight.
   SelfHostedFontTests holds both inventories to what the stylesheet actually asks for.

   Two unicode-range subsets each: `latin` covers French including the Œ/œ ligature, `latin-ext`
   covers the rest of Latin script. The ranges are the upstream ones, unedited, and are what makes
   the second subset free — a browser fetches a subset only once a character on the page falls in
   its range, so latin-ext costs nothing until a Station name or a NAS error text needs it. Anything
   outside both ranges falls to the fallback stack for that character alone.

   font-display: swap, not the default `auto`: an operator reading the page during an incident gets
   the text immediately in the fallback and the family a moment later, rather than a blank column
   while the file arrives. */

@font-face {
    font-family: "Archivo";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("../fonts/archivo-latin-400-normal.woff2") format("woff2");
    unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

@font-face {
    font-family: "Archivo";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("../fonts/archivo-latin-ext-400-normal.woff2") format("woff2");
    unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

@font-face {
    font-family: "Archivo";
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url("../fonts/archivo-latin-500-normal.woff2") format("woff2");
    unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

@font-face {
    font-family: "Archivo";
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url("../fonts/archivo-latin-ext-500-normal.woff2") format("woff2");
    unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

@font-face {
    font-family: "Archivo";
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url("../fonts/archivo-latin-600-normal.woff2") format("woff2");
    unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

@font-face {
    font-family: "Archivo";
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url("../fonts/archivo-latin-ext-600-normal.woff2") format("woff2");
    unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

@font-face {
    font-family: "Archivo";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("../fonts/archivo-latin-700-normal.woff2") format("woff2");
    unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

@font-face {
    font-family: "Archivo";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("../fonts/archivo-latin-ext-700-normal.woff2") format("woff2");
    unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

@font-face {
    font-family: "IBM Plex Mono";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("../fonts/ibm-plex-mono-latin-400-normal.woff2") format("woff2");
    unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

@font-face {
    font-family: "IBM Plex Mono";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("../fonts/ibm-plex-mono-latin-ext-400-normal.woff2") format("woff2");
    unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* RFSA-111: the token layer. One committed theme — dark — replacing the four --fleet-* variables,
   which collapsed distinctions the pages need: one hairline separated table rows *and* outlined
   panels, and one muted grey carried both a label and a note. Everything below is expressed in
   these tokens, so a page nobody has touched still comes out coherent.

   Every colour here is a measurement, not a preference. Each text tier clears WCAG AA (4.5:1)
   against every surface it can sit on, --surface-raised included — that is the table-header
   surface, the lightest thing on the page, and the one place the ramp gets tight. The ratios are
   recorded per token below; RFSA-109 rules they are verified by computation and not asserted in
   code, since a test that recomputes a ratio from a hex literal only restates the literal. */
:root {
    /* One theme, named. Not `light dark`, and not removed: this declaration is what makes the day
       selector's native <input type="date">, the role <select> on the Dashboard users page and the
       scrollbars render dark instead of flashing a light panel against a near-black page. It is the
       single most easily missed line in this change. */
    color-scheme: dark;

    /* Surfaces, a four-step ramp: the page, a card on it, a raised band inside a card (a table
       header), and an inset well (a form field, an inline value). Four steps rather than one plus
       borders, so a panel inside a panel is distinguishable without a second outline. */
    --surface-page: #0b0e14;
    --surface-card: #141922;
    --surface-raised: #1e2531;
    --surface-inset: #070910;

    /* Text, three deliberate tiers — value, label, note — so a reader has an order to read in.
       Worst case each, against the lightest surface (--surface-raised):
         --text  12.98:1   --muted  7.36:1   --dim  4.72:1
       On --surface-card those are 14.86 / 8.42 / 5.40, and on --surface-page 16.29 / 9.23 / 5.92.

       RFSA-115: those four numbers are the four *surfaces*, and a tier is only safe on a surface.
       A rule that puts a tint between the two — .health-badge's 14% fill of currentColor is the
       only one on the dashboard — has a backdrop none of these columns describes, and has to be
       measured on its own. That is how idle came to be the one reading under 4.5:1; see the
       .health-badge.health-idle rule. Add a translucent fill anywhere and re-measure what sits on
       it, rather than reading a tier off this block. */
    --text: #e8ecf2;
    --muted: #aab4c4;
    --dim: #868f9e;

    /* Two hairlines where there was one. --line outlines a panel, a field, a button, a Tile;
       --line-2 separates rows inside one, where a line as strong as the panel's own competes with
       the values it is separating. */
    --line: #3a4557;
    --line-2: #2a3342;

    /* The same three verdicts RFSA-83 settled, re-lit for a dark surface: one lightness and one
       chroma each — oklch(0.74 0.145 h), h = 150 / 72 / 26 — resolved to sRGB here so the values
       are checkable rather than gamut-mapped by the browser. Equal lightness is the point: no state
       shouts louder than another because of how its hue happens to render, so what an operator
       notices is the verdict and not the colour. On --surface-raised they measure 7.07 / 6.52 /
       6.27:1, and higher on every other surface.

       "degraded" stays amber rather than a second red, for the reason RFSA-83 gave: an operator has
       to be able to tell the Stations that are gone from the ones that are merely unwell, because
       they are different jobs and only one is urgent. */
    --ok: #5ec478;
    --warn: #e29a2c;
    --bad: #f9837a;

    /* RFSA-98's link blue (#0071c1, the per-Station dashboard's own `a` colour) re-lit for dark at
       the same hue: oklch(0.74 0.10 233). It replaces --fleet-nav-link and the color-mix lightening
       that rule needed — that mix existed because the literal blue measures ~2.8:1 on the masthead
       gradient, and this value clears 4.5:1 on the gradient (6.23:1 at its lightest point) and on
       every page surface (6.81:1 at worst), so one token now does both jobs. */
    --accent: #4fb7ea;

    /* RFSA-98: the local per-Station dashboard's own gradient (Service/Shared/MainLayout.razor.css's
       .sidebar), reused verbatim rather than an inspired-by palette, so the nav reads as the same
       product. Unchanged by this ticket on purpose — it is the one thing the two dashboards share,
       and both stops were already dark.

       The two endpoints stay verbatim. --fleet-nav-mid is not a third colour so much as the shortest
       path between them: the sidebar is a tall narrow column, so `180deg … 70%` sweeps navy to
       aubergine over ~900px of height and never dwells anywhere. This nav is a ~54px bar, so the
       same declaration ran the whole sweep out in 54px and then held flat aubergine for the bottom
       30% — the loudest block of colour on a page whose entire direction is "nothing outshouts a
       health colour". Running the sweep along the bar's long axis instead (see .main-nav) restores
       the sidebar's own pacing; the mid stop is what keeps the middle of that longer run from
       crossing through the muddy purple the two endpoints mix to on their own. */
    --fleet-nav-from: #052767;
    --fleet-nav-mid: #14184f;
    --fleet-nav-to: #3a0647;

    /* The graph-paper hairline behind every page. Deliberately weaker than anything the ramp above
       can produce — a 32px grid is a texture, and a texture an operator can read as a border is a
       bug. At this alpha a line resolves to rgb(16,20,28) over --surface-page, which costs the text
       tiers 0.3-0.4 of a point where one happens to sit on it: 15.55 / 8.81 / 5.65:1 against
       --text / --muted / --dim, versus the 16.29 / 9.23 / 5.92 the block above records for the flat
       surface. Recorded rather than assumed, for RFSA-115's reason — a tint between a tier and its
       surface has a backdrop none of that block's four columns describes. */
    --grid: rgba(126, 146, 196, 0.045);

    /* RFSA-118 acceptance criterion 5: three corners, and the reason there are only three is that
       the sheet had eleven. 0.2 / 0.25 / 0.3 / 0.35 / 0.5rem had accumulated one rule at a time,
       which at these sizes is not a scale an eye can read — it is five near-identical corners that
       cost the page any sense of a shape being repeated.

       The values are the RFSA-109 canvas's own (11-13px on a panel, 7-9px on a control, 4px inline),
       which is the larger half of what shipped. That is the whole visual difference between an admin
       table and a console, and it costs one declaration each.

       A pill is not on this scale: 999px is a shape, not a size, and .health-badge and .grid-sort a
       take it directly. */
    --radius-panel: 0.7rem;
    --radius-control: 0.45rem;
    --radius-inline: 0.25rem;

    /* RFSA-110: the two families, behind a token each so the fallback stack has one home and cannot
       drift between the rules that use it.

       Each stack names the self-hosted family first and then real families, ending in the matching
       generic — not a bare `Archivo`, which falls all the way to the browser's default if the file
       never arrives, and that default is a serif on most engines. A missing font file has to cost
       this page a typeface, not its character. */
    --font-ui: "Archivo", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --font-mono: "IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;
}

/* The page itself, and the one place on the dashboard that is decoration rather than a reading.
   Two things at once, both from the RFSA-109 canvas and both kept at a weight that cannot compete
   with a health colour:

   The graph paper. A 32px grid drawn as two 1px gradient stripes rather than an image, so it costs
   no request and cannot go missing. It gives the surfaces something to sit *on* — every panel on
   this dashboard is a near-black card on a near-black page, and without a texture behind them the
   only thing saying "card" is a 1.99:1 hairline (which is RFSA-116's whole subject).

   The glow. One cold ellipse behind the masthead, anchored off the top-left so its own centre is
   never on screen and what shows is the falloff. Its peak resolves to rgb(15,28,58), which is where
   the --dim tier bottoms out at 5.16:1 — the lowest reading this rule creates anywhere, and still
   AA. It sits mostly *behind* the opaque nav on purpose: what an operator sees is the light leaking
   out from under the masthead, not a blue blob on the page.

   background-color and background-image apart rather than one `background:` shorthand — the
   shorthand resets the layers, and the next rule that sets a page colour would silently take the
   texture with it. */
body {
    margin: 0;
    padding: 1.5rem;
    background-color: var(--surface-page);
    background-image:
        radial-gradient(68rem 24rem at 18% -8%, rgb(24 62 145 / 0.30), transparent 72%),
        linear-gradient(var(--grid) 1px, transparent 1px),
        linear-gradient(90deg, var(--grid) 1px, transparent 1px);
    background-size: auto, 100% 2rem, 2rem 100%;
    /* The glow is one ellipse, not a tiled one: without this it repeats down a long fleet page and
       the page grows a horizon every 24rem. */
    background-repeat: no-repeat, repeat, repeat;
    /* Scrolling with the page, which is the default and is left unstated deliberately: `fixed` here
       looks right on the fleet page's first screen and wrong the moment anyone scrolls, because the
       ruling then slides underneath the Tiles instead of travelling with them. Paper does not
       parallax. The same default sends the glow up and out of view on a long page, which is also
       right — it belongs to the masthead, not to the viewport. */
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 0.95rem;
    line-height: 1.5;
}

/* One ring, declared once on the bare pseudo-class so it reaches every control on every page —
   including the ones that are only focusable because of what they are (a <summary>, a link), which
   no per-component rule would have thought to cover. --accent because it is the one colour on the
   page that carries no verdict, so a focus ring can never be read as a health state. */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* The page's link colour. Rules that need a link to read as something else — a column header, a
   Tile's own heading, a day-selector control — override this with `color: inherit` and are more
   specific, so this is only ever the default for a link that really is prose. */
a {
    color: var(--accent);
    text-underline-offset: 0.15em;
}

h1 {
    font-size: 1.4rem;
    font-weight: 700;
    margin: 0 0 0.25rem;
}

/* RFSA-98: full-bleed to the viewport edge (negative margin cancels body's own padding) so the
   gradient reads as a top navbar rather than a tinted paragraph inside the page's content column.

   100deg rather than 180deg, with the same two endpoints and the mid stop the token block explains:
   the sweep runs along the bar's long axis, which is the axis the sidebar it is borrowed from
   actually has. The lift and the shadow are what separate the bar from the page now that the page
   has a texture of its own — a flat --line hairline under a gradient reads as the end of the
   gradient rather than as an edge. */
.main-nav {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin: -1.5rem -1.5rem 1.5rem;
    padding: 0.8rem 1.5rem;
    background-image: linear-gradient(
        100deg,
        var(--fleet-nav-from) 0%,
        var(--fleet-nav-mid) 46%,
        var(--fleet-nav-to) 100%);
    border-bottom: 1px solid rgb(255 255 255 / 0.10);
    box-shadow:
        inset 0 1px 0 rgb(255 255 255 / 0.07),
        0 1.1rem 2.5rem -1.9rem #000;
    color: #e7e7ee;
}

/* The mark and the links are one group at the bar's left end; the signed-in bar is the other. Two
   children rather than three, so `justify-content: space-between` above puts the gap where the bar
   actually divides instead of spreading three items evenly across it. */
.nav-left {
    display: flex;
    align-items: center;
    gap: 1.6rem;
    flex-wrap: wrap;
}

/* Who this is. The dashboard had no mark at all — an operator arriving at a full-screen page of
   Tiles on a wall display had nothing on it naming the product, and the one <h1> that does say so
   is the fleet page's alone. Set in the same near-white the bar's own text is, not in --accent:
   a mark that reads as a link invites a click that goes nowhere. */
.brand {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    color: #d3dbef;
}

.brand svg {
    display: block;
    flex: none;
}

/* Small, letter-spaced caps rather than a heading: this names the product beside the nav, and
   anything heavier would compete with the page's own <h1> two rows below it. 700 is a weight the
   UI family ships (SelfHostedFontTests pins that). */
.brand-word {
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.17em;
    text-transform: uppercase;
    white-space: nowrap;
}

/* Tight rather than the 1rem this used to be. The links carry their own padded hit area now, so a
   wide gap on top of it spread five items across half the bar and left the group reading as five
   separate links rather than as one nav. */
.main-nav-links {
    display: flex;
    gap: 0.15rem;
    flex-wrap: wrap;
}

/* Not --accent, and that is a reversal of what RFSA-98 settled — worth saying why. --accent was
   chosen to clear AA on the gradient (6.23:1 at worst) where the literal #0071c1 measured ~2.8:1,
   and it does. But it is also the page's link colour, so five cyan links across the masthead read
   as five links that happen to be up there rather than as the nav, and they were the brightest
   thing on the page. #b6c4e5 is the bar's own text a step down — 8.05 / 9.44 / 9.27:1 across the
   three stops, so this is more contrast than the token it replaces, not less. What says "link" is
   the hover fill and the hit area; --accent stays the marker's underline below, where being the
   page's accent is the point.

   500 rather than 600: the labels are set against a gradient rather than a surface, where a heavier
   weight thickens into it. The UI family ships both (SelfHostedFontTests pins that). */
.main-nav-links a {
    color: #b6c4e5;
    text-decoration: none;
    font-weight: 500;
    padding: 0.3rem 0.6rem;
    border-radius: var(--radius-control);
}

/* :focus-visible rather than :focus, unlike the rest of this stylesheet's link rules: a nav link
   clicked with the mouse keeps focus across the client-side navigation it just caused, so with
   :focus the page an operator has left would stay highlighted beside the marker on the page they
   arrived at — two links lit, one of them wrong. */
.main-nav-links a:hover,
.main-nav-links a:focus-visible {
    background-color: color-mix(in srgb, var(--accent) 22%, transparent);
    color: white;
}

/* RFSA-111 acceptance criterion 5: the page an operator is on. Needed because the nav is
   persistent and a Station drill-down link leaves them somewhere they did not choose from it.

   Two cues rather than one, and neither is the hover treatment: the label goes to full white
   (which no other nav state uses) and the accent underlines it from inside. NavLink is what sets
   the class, so the marker follows a client-side navigation as well as a full page load. */
/* Three cues now, where RFSA-111 named two, and the third is what the change above cost it: with
   every link cyan, "full white" was a large step. Against #b6c4e5 it is a smaller one, so the pill
   fill and its hairline ring carry the marker and the accent underline stays as the one thing no
   other nav state has. The ring is what keeps the pill legible on the aubergine end of the
   gradient, where a 10% white fill alone is nearly invisible. */
.main-nav-links a.active {
    color: white;
    background-color: color-mix(in srgb, white 11%, transparent);
    box-shadow:
        inset 0 0 0 1px rgb(255 255 255 / 0.13),
        inset 0 -2px 0 var(--accent);
}

/* --muted's job, said in the bar's own palette rather than with the page token: 6.10:1 at the
   gradient's lightest stop. "Signed in as" is a label, and it was competing with the address it
   labels. */
.signed-in-bar {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    font-size: 0.8rem;
    color: #9dabca;
}

.signed-in-bar form {
    display: inline;
}

/* The address itself: monospaced, because it is an identifier rather than prose, and a step back up
   the bar's ramp (8.92:1) so the reading outranks its own label. Regular weight — the monospace
   family ships regular only, which SelfHostedFontTests pins. */
.signed-in-bar .who {
    font-family: var(--font-mono);
    color: #c3cee8;
}

/* Signing out has to be a POST (not a plain <a> GET, which any third-party page could trigger).
   It used to be dressed as an inline link to sit quietly in this bar; on a bar that now has a
   mark, a nav group and a monospaced address, an underlined word at the far right read as the
   fourth kind of text in a row rather than as the one control there. A quiet outlined button is
   the honest shape for it — 8.68:1 at the worst stop, over its own face. */
.link-button {
    font: inherit;
    font-size: 0.75rem;
    color: #cfd9f2;
    background: rgb(255 255 255 / 0.05);
    border: 1px solid rgb(255 255 255 / 0.17);
    border-radius: var(--radius-control);
    padding: 0.25rem 0.55rem;
    cursor: pointer;
}

.link-button:hover,
.link-button:focus {
    background: rgb(255 255 255 / 0.11);
    color: white;
}

.subtitle {
    color: var(--muted);
    margin: 0 0 1.25rem;
}

/* RFSA-118 acceptance criterion 6: the fleet page's masthead — the title and its subtitle at one
   end, the fleet-wide reading at the other. flex-end rather than centre, so the panel's bottom edge
   lands on the subtitle's baseline row and the two read as one band however tall either gets.

   Wrapping is the small-viewport answer and needs no query: below about 60rem the panel drops to a
   line of its own, which is exactly the full-width strip RFSA-114 shipped. */
.page-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    flex-wrap: wrap;
    gap: 1rem 1.75rem;
    margin: 0 0 1.4rem;
}

/* The h1 and the subtitle travel together as the left half. min-width:0 so a long subtitle wraps
   inside this column instead of forcing the panel off the row. */
.page-head-titles {
    min-width: 0;
}

/* The subtitle's own bottom margin is the gap under the masthead on the other eight pages. Inside
   .page-head that gap belongs to the flex container, and leaving it here would push the panel's
   baseline a line below the text it sits beside. */
.page-head .subtitle {
    margin-bottom: 0;
}

.subtitle {
    color: var(--muted);
    margin: 0 0 1.25rem;
}

/* RFSA-114's counters, as RFSA-118 acceptance criterion 6 shapes them: one instrument with four
   cells rather than four separate chips. The hairlines are internal and the corner is on the
   container, so what an operator sees is a single readout that happens to be divided — which is what
   four counts of one fleet are. `overflow: hidden` is what lets the segments square their own
   corners and still sit inside a rounded box.

   The four verdict colours are not in this block. They come from the .health-* rules at the bottom
   of this file, which a segment wears alongside .fleet-counter, so a count and a Tile's health badge
   cannot end up two hues for one verdict. Shape here, verdict there — the same split .station-tile
   already makes with its own health rules. */
.fleet-counters {
    display: flex;
    align-items: stretch;
    border: 1px solid var(--line);
    border-radius: var(--radius-panel);
    background: var(--surface-card);
    overflow: hidden;
}

/* One cell. The number over the word rather than beside it, which is what buys the panel its height
   and lets four counts sit in the masthead without a bar of their own.

   min-width, not flex: a strip whose segments resize as the counts change is a strip whose four
   positions move, which is the one thing acceptance criterion 2 rules out. The floor is set by the
   widest label ("degraded") rather than by the digits, so a fleet going from 9 to 10 anywhere moves
   nothing. nowrap for the same reason, since "on air" is one token. */
.fleet-counter {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 6rem;
    padding: 0.5rem 1rem 0.55rem;
    border-left: 1px solid var(--line);
    white-space: nowrap;
}

.fleet-counter:first-child {
    border-left: 0;
}

/* Monospaced and tabular, because these digits are rewritten in place every four seconds by the
   grid's own poll: a proportional "1" is narrower than a "4", so a fleet ticking between them would
   shift every label under it. The one reading on this page that changes under an operator's eye is
   the one that must not move.

   --text, not the verdict hue: the number is the reading and the word beneath it is the verdict, so
   coloured digits would say the same thing twice and leave the label with nothing of its own. */
.fleet-counter-label,
.fleet-counter-value {
    line-height: 1.1;
}

.fleet-counter-value {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 1.25rem;
    font-weight: 600;
    color: var(--text);
}

/* The verdict, and where the hue now lands. Letter-spaced caps rather than running text, because
   this is a label under a reading rather than a sentence — the same treatment .tile-fact-label
   carries for the same reason. Colour comes from the .health-* rules the segment wears. */
.fleet-counter-label {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

/* The verdict said in something other than the colour of a word (acceptance criterion 3), and the
   same dot .health-badge carries since RFSA-118 acceptance criterion 4 — a count and a Tile now
   read as one verdict in one shape. Painted with currentColor, so the segment's own .health-* class
   is the single place the hue is decided and the dot cannot drift from the word beside it. */
.fleet-counter-label::before {
    content: "";
    flex: none;
    width: 0.4rem;
    height: 0.4rem;
    border-radius: 50%;
    background: currentColor;
}

/* Acceptance criterion 5's marker, rendered on past days only. A gloss, not a fifth segment: no
   hairline, no dot, no cell width, and set in the note tier — because anything that made it look
   like a segment would have an operator reading five places where the criterion promises four.
   It is inside the panel because the strip's children are all spans by test (that is what delimits
   it), so what keeps it from reading as a cell is the treatment rather than the position. */
.fleet-counters-live {
    display: flex;
    align-items: center;
    /* The dash below is a flex item now, and flex trims the trailing space its content string
       carries — "—right now" rather than "— right now". The gap puts it back as layout, which is
       the only place it can live where a copy edit to the note cannot lose it again. */
    gap: 0.3rem;
    padding: 0 0.9rem 0 0.7rem;
    color: var(--dim);
    font-size: 0.8rem;
    font-style: italic;
}

/* The dash belongs to the aside, not to the last count — written as generated content so the strip's
   markup stays four segments and a note, with no punctuation element between them. */
.fleet-counters-live::before {
    content: "— ";
}

/* RFSA-101: the dashboard-wide day selector, above the grid it scopes. Two rows rather than one
   (RFSA-113 acceptance criterion 3): the controls, then the retention statement on a line of its
   own beneath them. `margin-left: auto` used to push that statement to the far end of the same
   row, where it still read as the last item in a row of controls — and, once the row wrapped, as a
   control that had dropped to a second line. A note about a group of controls belongs under the
   group, not in it. */
/* RFSA-118 acceptance criterion 3: the controls and their retention note sit on a surface of their
   own, where they used to sit loose on the page. This was defensible while the page was a flat
   colour; with the graph paper behind it, a row of controls with nothing under them reads as
   floating, and the one group on the fleet page that scopes everything below it looked like the
   least deliberate thing on it.

   The panel is --surface-card, which is what moves .day-nav's own segments up a step to
   --surface-raised below — an instrument on a card, rather than an instrument the same colour as
   the card it is on. That is the ramp doing the job the token block describes. */
.day-selector {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem 0.6rem;
    margin: 0 0 1rem;
    padding: 0.6rem 0.7rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-panel);
    background: var(--surface-card);
}

/* RFSA-113 acceptance criterion 1: Prev, the date, Next and Today are one instrument, not four
   loose chips that happen to sit beside each other. One outline and one corner radius around the
   whole group, and the dividers between its segments are the container's own background showing
   through 1px flex gaps — no per-segment border, so no rule has to win a specificity argument with
   the `input` primitive's border shorthand to keep a divider where it belongs. */
.day-nav {
    display: flex;
    align-items: stretch;
    gap: 1px;
    border: 1px solid var(--line);
    border-radius: var(--radius-control);
    background: var(--line-2);
}

/* The end segments are rounded individually rather than clipped by `overflow: hidden` on the group,
   which is the obvious way to do this and is wrong here: the focus ring is drawn 2px *outside* an
   element's border box (see :focus-visible), so a clipping container would cut the ring off the
   first and last segments — exactly the two an operator reaches first with the keyboard.

   The radius is 0.4rem against the group's --radius-control (0.45rem): one border-width less, so the
   segment's corner sits concentric inside the group's rather than crossing it. Written as a literal
   rather than a fourth token, because it is not a step on the scale — it is this scale's control
   radius minus this group's border, and it has to move with either. The date input is deliberately not
   covered — it can never be the first or last child, since Prev always precedes it and Next always
   follows — and its own rule below zeroes the radius the `input` primitive would otherwise give it. */
.day-nav > :first-child {
    border-start-start-radius: 0.4rem;
    border-end-start-radius: 0.4rem;
}

.day-nav > :last-child {
    border-start-end-radius: 0.4rem;
    border-end-end-radius: 0.4rem;
}

/* --surface-raised, not --surface-card: RFSA-118 put a card under this instrument, and a segment
   the same colour as the panel it sits on has only its hairline left to say it is a control.
   12.98:1 for --text there, which is the figure the token block records for that surface. */
.day-step,
.day-today {
    display: flex;
    align-items: center;
    padding: 0.25rem 0.6rem;
    background: var(--surface-raised);
    color: var(--text);
    text-decoration: none;
    font-weight: 600;
}

/* The same hover the `button` primitive takes, because a segment of this instrument does the same
   job a button does. Written against `a` so it cannot reach the disabled span, which is not one. */
a.day-step:hover,
a.day-step:focus,
.day-today:hover,
.day-today:focus {
    background: color-mix(in srgb, var(--accent) 14%, var(--surface-raised));
    text-decoration: none;
}

/* A span rather than a disabled <a> at the bounds: an anchor with no href is not focusable and
   announces itself as plain text, which is the honest reading of "there is nowhere to go".

   RFSA-113 acceptance criterion 2. Grey text was the whole of the old cue, and grey text inside a
   row of grey-bordered chips is a difference an operator finds by comparing two chips rather than
   by looking at one. The segment now recedes *below* the instrument's own face — --surface-inset,
   the same well a form field is sunk into — so an unavailable bound reads as a slot with nothing in
   it. The weight drops back to normal for the same reason: every live segment is 600, so a bound
   that cannot be clicked should not be as loud as one that can. */
.day-step.disabled {
    background: var(--surface-inset);
    color: var(--dim);
    font-weight: 400;
    cursor: not-allowed;
}

/* Flush inside the instrument: no border and no corner of its own, since the group owns both, and
   monospaced with tabular figures because an ISO date is a machine value like any other stamp on
   this page — see the .mono rule.

   Still a native <input type="date"> (color-scheme: dark is what makes its picker match) rather
   than a styled box with a fake calendar glyph, which is what the RFSA-109 canvas draws — a mockup
   can draw the field it wants; a page that replaces the native control takes the operator's own
   date picker away with it, and that is a real loss for a fake gain.

   What RFSA-118 does change is which surface it is: --surface-inset rather than the panel face, so
   the one segment of this instrument an operator types into reads as a well sunk into it, the same
   way every other field on the dashboard does. --text measures 16.78:1 there, the highest reading
   on any of the four surfaces. */
.day-nav input[type="date"] {
    border: 0;
    border-radius: 0;
    padding: 0.25rem 0.5rem;
    background: var(--surface-inset);
    color: var(--text);
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}

/* The picker glyph is a UA-drawn button, and under color-scheme: dark it arrives at full white —
   the brightest thing in a control group whose own labels are one tier down. Held back to the label
   tier at rest and given all of it back on hover and focus, so it stops competing with the date it
   opens without ever becoming hard to find. Chromium-only by nature; a browser without this
   pseudo-element renders the glyph exactly as it does today. */
.day-nav input[type="date"]::-webkit-calendar-picker-indicator {
    opacity: 0.55;
    cursor: pointer;
}

.day-nav input[type="date"]:hover::-webkit-calendar-picker-indicator,
.day-nav input[type="date"]:focus::-webkit-calendar-picker-indicator {
    opacity: 1;
}

/* RFSA-118: back to the far end of the controls' own row, which is where RFSA-113 moved it *from*.
   That ticket's argument was sound for the block it was looking at and stops being sound once the
   block has an outline. It had two halves:

   "At the far end it still read as the last item in a row of controls." That was true of a bare row
   on the page, where nothing said where the group began or ended, so the rightmost thing in the row
   was read as part of it. The panel says it now — the controls are raised chips on a card and this
   is --dim text at 0.8rem with no face of its own, which is a difference visible in one glance
   rather than by comparing two items.

   "Once the row wrapped it read as a control that had dropped to a second line." Still worth
   avoiding, and `margin-left: auto` is what avoids it rather than what causes it: when the row runs
   out of room the note wraps alone, and the auto margin keeps it right-aligned on that line, where
   nothing sits beside it to be mistaken for. Which is the layout RFSA-113 wanted — the difference is
   that it is now the narrow-viewport case instead of every case. Measured rather than assumed: it
   shares the controls' row down to ~820px and is alone on its own line at 700 and below, with no
   width in between where it sits beside a single stray control.

   What forced the revisit is that the panel is full-bleed: with the note underneath, the whole right
   half of it was empty, so the one group that scopes everything below it read as the least finished
   thing on the page. max-width in ch because this is a sentence — it wraps to two lines against the
   controls' height rather than running the width of the console. */
.day-retention {
    margin-left: auto;
    max-width: 46ch;
    color: var(--dim);
    font-size: 0.8rem;
    line-height: 1.4;
    text-align: right;
}

/* Amber, not red: viewing a past day is a legitimate thing to do, and the notice is a clarification
   of what the grid below means, not a fault.

   RFSA-113 acceptance criterion 5: the same division RFSA-111 made inside an alert panel. A whole
   paragraph set in --warn is a page shouting a colour that means "something is wrong" at a reader
   who has merely picked a date; the panel and its lit edge are what say "read this", and the
   sentence itself returns to the value tier so it can be read as the caveat it is. */
.day-live-notice {
    margin: 0 0 1rem;
    padding: 0.6rem 0.8rem;
    border: 1px solid var(--line);
    border-left: 3px solid var(--warn);
    border-radius: var(--radius-panel);
    background: var(--surface-card);
    color: var(--text);
}

/* RFSA-104: the two orderings, now that the table headers that carried them are gone. Kept above
   the grid rather than on each Tile — the sort is a property of the grid, not of a Station. */
.grid-sort {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin: 0 0 0.75rem;
    font-size: 0.9rem;
}

.grid-sort-label {
    color: var(--muted);
}

/* The `th a` rule below gave these controls their look while they were column headers; they are no
   longer inside a th, so without this they render as default underlined blue links.

   RFSA-113 acceptance criterion 4. Keeping the header treatment after the header was gone left two
   words in body text with nothing around them: nothing said they were clickable until the pointer
   was already on one, and the sort arrow was the only mark on the active one — a single character
   at the end of a word, which is not a state an operator sees without reading. Each ordering is now
   a chip an operator can see is a control before touching it. */
.grid-sort a {
    color: inherit;
    text-decoration: none;
    font-weight: 600;
    padding: 0.15rem 0.5rem;
    border: 1px solid var(--line-2);
    border-radius: 999px;
    background: var(--surface-card);
}

/* The active ordering, marked with the accent from inside — the same idea the nav uses for the page
   an operator is on (.main-nav-links a.active), since "this is the one you are on" is one idea and a
   dashboard that says it two ways makes a reader learn two.

   RFSA-118 made these chips pills, and a pill cannot take the nav's bottom underline: an inset
   shadow follows the border-radius, so `inset 0 -2px 0` on a 999px corner is not a rule under a
   word but a crescent under it. The accent becomes a ring instead, which is the same statement in
   the shape this control actually has. The nav keeps the underline, because a nav link is a
   rounded rectangle with a flat bottom edge to underline — the idiom is "the accent marks it", and
   that is what both still do. The arrow SortIndicator appends stays, so the mark is never colour
   alone.

   Keyed on aria-current rather than a class: the attribute is what tells a screen reader the same
   thing the fill tells everyone else, so styling it is what keeps the two from drifting apart.

   Above the hover rule, not below it, and that ordering is load-bearing: the two selectors have the
   same specificity, so whichever comes last wins the `background` and `border-color` they both set.
   Below, this rule would have taken the pointer response away from the active chip — which is still
   a control, since clicking it reverses the direction — leaving the one ordering an operator is
   using as the one that does not answer the mouse. The marker itself survives the hover regardless:
   `box-shadow` is set here and nowhere else. */
.grid-sort a[aria-current="true"] {
    background: var(--surface-raised);
    border-color: var(--line);
    color: var(--text);
    box-shadow: inset 0 0 0 1.5px var(--accent);
}

/* The `button` primitive's hover again, for the same reason the day-nav segments take it: these do
   what a button does. `text-decoration: none` overrides the old underline-on-hover — the chip's own
   border and fill now answer the pointer, and an underline on top of them is a second answer. */
.grid-sort a:hover,
.grid-sort a:focus {
    border-color: color-mix(in srgb, var(--accent) 55%, var(--line));
    background: color-mix(in srgb, var(--accent) 14%, var(--surface-raised));
    text-decoration: none;
}

/* auto-fill, not auto-fit: a fleet of two Stations should read as two Tiles of the same width as a
   fleet of forty, rather than two Tiles stretched across the whole viewport. */
.tile-grid {
    display: grid;
    /* 28rem rather than 26: a Break row is seven columns since RFSA-106, and the widest of them are
       floored at what their own values need. Below 28rem those floors no longer fit and the Tile
       falls back to scrolling sideways, which is worse than fitting one fewer Tile per row.

       RFSA-115: `min(28rem, 100%)` rather than a bare 28rem, and the difference only shows up at
       one width. A bare minmax() floor is a hard one — auto-fill cannot drop below a single
       track, so once the viewport left less than 28rem for it the track stayed 448px wide and the
       *whole page* scrolled sideways: measured at 480px and below (9px of overflow at 480, 114px
       at 375), where every column had already reflowed to one and there was no "one fewer Tile per
       row" left to fit. min() caps the floor at the track's own share of the row, so the last
       column shrinks with the viewport and the overflow lands where the comment above always said
       it should — inside .station-tile's own `overflow-x: auto`, on the one Tile that cannot fit
       its Break row, instead of on the page. Above 496px nothing changes: 100% exceeds 28rem
       there, so min() resolves to 28rem and the reflow is the one every other width already had. */
    grid-template-columns: repeat(auto-fill, minmax(min(28rem, 100%), 1fr));
    gap: 1rem;
    align-items: start;
}

.station-tile {
    /* The health colour and the light it throws, both defaulted here and both set per health at the
       bottom of this file. Declared on the Tile rather than only on the four health rules so that a
       Tile carrying no health class still has a defined edge instead of inheriting one, and so the
       box-shadow below can be written once. */
    --tile-edge: var(--line);
    --tile-glow: transparent;

    border: 1px solid var(--line);
    /* The lit edge: a crisp 4px line in the health colour, plus the light it throws inward.
       Border-plus-shadow rather than an element drawn over the content, and that is forced rather
       than chosen (acceptance criterion 2) — this Tile scrolls its Breaks sideways, so a positioned
       glow would either scroll away with them or be clipped by the radius. Both of these are
       properties of the Tile's own box: an inset shadow is painted on the padding box and stays
       where it was put however far the content inside has scrolled. */
    border-left: 4px solid var(--tile-edge);
    /* The blur is deliberately shorter than the 0.9rem of left padding below it, so the glow lives
       and dies inside the Tile's own left gutter. Two reasons, both of which cost a longer glow
       something real. An inset shadow paints in its element's *background* phase, under every
       descendant — so a glow reaching into the content box is cut into stripes by the first opaque
       child background it meets, and there are two: .break-row's hover lift and .break-files' well,
       each spanning the full content width. And any --dim text sitting in the lit band is composited
       over a verdict colour, which quietly costs it some of the contrast the token block records as
       measured. Keeping the light in the gutter means no child can occlude it and no text is ever on
       top of it, so the recorded ratios stay true and the edge reads as one unbroken line. */
    box-shadow: inset 0.9rem 0 0.8rem -0.9rem var(--tile-glow);
    border-radius: var(--radius-panel);
    padding: 0.7rem 0.9rem;
    /* A card on the ramp, where the Tile used to sit directly on the page. The Tile is what
       --surface-card was measured for, and the step matters twice over now: the lit edge has a
       surface to be lit against, and .break-row's hover lift to --surface-raised reads as a lift
       out of a card rather than as a band appearing on the page. */
    background: var(--surface-card);
    /* The Breaks table can outgrow a narrow Tile; scroll it inside rather than widening the Tile
       and breaking the grid's columns. */
    overflow-x: auto;
}

.tile-header {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.5rem;
    margin-bottom: 0.5rem;
}

.tile-name {
    font-size: 1.05rem;
    font-weight: 600;
    margin: 0;
}

/* The Station name is the drill-down's entry point (RFSA-82), but it is also this Tile's heading, so
   it must not render in the page's own link colour: as an accent-blue heading on a Tile whose whole
   left edge is already carrying the health colour, it reads as a badge rather than as a name. Same
   treatment as .grid-sort a and th a — the text takes the page's own colour — except that the
   underline stays at rest rather than appearing on hover, because on a heading it is the only
   remaining cue that this navigates somewhere. */
.tile-name a {
    color: inherit;
    text-decoration-color: var(--line);
    text-underline-offset: 0.15em;
}

.tile-name a:hover,
.tile-name a:focus {
    text-decoration-color: currentColor;
}

/* What a Tile carries instead of a health badge on a past day (RFSA-104 AC2). Deliberately quiet:
   it explains the absence of a colour, and is not itself a status. */
.tile-health-note {
    color: var(--dim);
    font-size: 0.8rem;
    white-space: nowrap;
}

.tile-empty {
    margin: 0.25rem 0 0.5rem;
    color: var(--dim);
}

/* "Never received" is the Fleet Dashboard reporting a gap in its own data, not the Station
   reporting a quiet day — so it is marked, where "reported no Breaks" reads as ordinary muted
   text. Amber rather than red for the same reason .day-live-notice is: it is a caveat about what is
   on screen, not a fault to go and fix. */
.tile-unavailable {
    border-left: 3px solid var(--warn);
    padding-left: 0.5rem;
    color: var(--warn);
}

.tile-breaks {
    font-size: 0.85rem;

    /* No column is content-sized (`auto` / `max-content`). The header and the rows are separate
       grids now rather than one table, and a content-sized track resolves per grid — so break 1,
       break 2 and the header would each pick their own widths and the columns would stagger.

       The three stamp columns and Status are minmax(floor, fr): the floor is the widest thing that
       column ever holds, so nothing can be squeezed into wrapping the way "Completed" was, and the
       fr makes the tracks share out whatever is left so the row ends exactly at the Tile's edge —
       which is what puts the rule under the header all the way across rather than stopping short of
       it. Every row resolves those frs against the same width, so they still line up.

       The floors add up to ~46ch, which is why .tile-grid's minimum Tile is 28rem: below that the
       row cannot fit and would need the horizontal scroll .station-tile still keeps as a fallback.

       ch is the advance of "0" in *this element's* font, so RFSA-110 changing the family changed
       every floor here. Re-checked rather than assumed: Archivo's digit advance is 0.573em against
       Segoe UI's ~0.55, so at this rule's 0.85rem the floors come to ~358px, plus ~29px of gaps —
       inside the ~414px a 28rem Tile has left after its padding and borders, with about 27px spare.
       A wider family, or a larger font-size here, is what would tip the row into scrolling. */
    --break-columns:
        4.6ch                 /* Break, including the disclosure marker */
        3.6ch                 /* F1 — "yes" or a dash; the header is abbreviated to match */
        minmax(9.2ch, 1.1fr)  /* Status — "Completed" is the widest value on the row */
        minmax(7.6ch, 1fr)    /* Pushed */
        minmax(7.6ch, 1fr)    /* Played */
        minmax(9ch, 1fr)      /* Completed — sized by its own header, not by its stamps */
        4.2ch;                /* Files */
}

.tile-breaks-head,
.break-row {
    display: grid;
    grid-template-columns: var(--break-columns);
    gap: 0 0.3rem;
    align-items: baseline;
    padding: 0.2rem 0;
    /* --line-2 rather than a mix of the panel's own hairline: this separates rows inside a Tile,
       and a line as strong as the Tile's outline competes with the values it is separating. */
    border-bottom: 1px solid var(--line-2);
}

.tile-breaks-head {
    color: var(--dim);
    /* A header never wraps: these are one-word labels sized into their own tracks, and a wrapped
       "Completed" would push the header row taller than the rows it labels. */
    white-space: nowrap;
}

/* The whole Break row is the <summary>, so it is the click target, the focus target and what a
   screen reader announces — none of which needed a handler, a tabindex or an ARIA attribute. */
.break-row {
    cursor: pointer;
    /* Both halves are needed to drop the native disclosure triangle: `display: grid` already
       suppresses it in most engines, but WebKit needs its own pseudo-element told as well. The
       marker is re-drawn inside the first cell instead, where it costs no column of its own. */
    list-style: none;
}

.break-row::-webkit-details-marker {
    display: none;
}

/* A lift up the surface ramp rather than a tint of the hairline: on a dark Tile the row under the
   pointer has to get lighter to read as the one being pointed at.
   Acceptance criterion 8: the NAS sync line takes it too. A Tile has exactly two expanders on it and
   an operator has to recognise both as one control — the sync line is where they go to find out
   which NAS folder failed, and it was the one clickable line on the Tile that did not respond to a
   pointer. It gets the row's vertical padding below so the lift lands as the same band. */
.break-row:hover,
.break-row:focus-visible,
.tile-sync summary:hover,
.tile-sync summary:focus-visible {
    background: var(--surface-raised);
}

/* The disclosure marker, for both of a Tile's expanders — the Break row draws it inside its first
   cell, where it costs no column of its own, and the NAS sync line in front of its label. One rule
   rather than one each, because "the two expanders on a Tile read as the same control" (acceptance
   criterion 8) is a claim two rules can quietly stop honouring. The fixed width is what keeps the
   glyph swap below from shifting the text after it. */
.break-index::before,
.tile-sync summary::before {
    content: "\25B8";
    display: inline-block;
    width: 0.9em;
    color: var(--dim);
}

.break[open] > .break-row .break-index::before,
.tile-sync details[open] > summary::before {
    content: "\25BE";
}

/* Default wrapping, deliberately — not `overflow-wrap: anywhere`, which is what broke "Completed"
   across two lines. Ordinary wrapping breaks at spaces only, and the one value here that contains a
   space is the dated stamp a Break carries when it falls outside its own Station Day ("MM-dd
   HH:mm:ss", RFSA-104). So a plain "06:37:22" and a plain "Completed" can never break, while the
   rare dated stamp still wraps after its date instead of overrunning the next column. */
.break-cell {
    min-width: 0;
}

/* Acceptance criterion 4: the three lifecycle stamps — Pushed, Played, Completed — monospaced with
   tabular figures. Proportional digits let 06:20:10 and 11:02:17 resolve to different widths, so
   nothing lines up down the column and a digit that changed cannot be spotted without reading every
   row; that is the whole reason .mono exists, and tabular-nums is stated for the reason that rule
   gives — it is what keeps the column aligning when it renders on the fallback stack.

   Selected by track position rather than by a class on the span, because which columns hold a stamp
   and how wide those columns are is one fact with one home: --break-columns above, whose seventh
   entry is the count in this selector. A class would put half of it in Index.razor.

   The header keeps the UI family deliberately. Its widest label sizes its own track (--break-columns
   floors Completed at 9ch by its header, not by its stamps), and 9 monospace characters need
   9.4ch — so a monospaced header is a wrapped header.

   The stamps themselves fit without touching a floor, which acceptance criterion 5 requires. They
   do need part of the fr share to: --break-columns' floors are in ch, so they are measured in
   Archivo's digit advance, and IBM Plex Mono's is wider (0.6em against ~0.57em) — an 8-character
   "HH:mm:ss" is a little past the 7.6ch Pushed and Played floor. Every one of these tracks is
   minmax(floor, fr), and the fr share covers the difference with room over: verified by rendering a
   Tile at exactly the 28rem minimum, where each stamp clears its own track on both sides and the
   header still lines up with every row. The one stamp that does not fit is the dated form a Break
   outside its own Station Day carries ("MM-dd HH:mm:ss", RFSA-104), and it is not meant to — it
   wraps after its date, which is the case .break-cell above keeps ordinary word-wrapping for, and
   both halves then clear the track.

   The family and the tabular figures themselves are not declared here: this selector joins .mono's
   own rule below, which is the utility RFSA-110 built for exactly these columns. Re-declaring its
   two lines here instead would read the same today and drift tomorrow — the next ticket to tune
   .mono (a tracking correction for the fallback stack, slashed-zero) would leave the one place the
   utility was made for behind, with SelfHostedFontTests still green because it only ever reads
   .mono. Only the size step is local, because only it is about this row.

   0.95em rather than 1em is not that arithmetic, though it helps it: it is the step `code` below
   measured for the same pairing, IBM Plex Mono's x-height sitting under Archivo's. */
.break-row > .break-cell:nth-child(n+4):nth-child(-n+6) {
    font-size: 0.95em;
}

/* Acceptance criterion 6: weight, and nothing else. These labels are lifecycle names, not verdicts —
   a Break sitting at Pushed at 08:30 is not a fault — and a second colour system on the row would
   compete with the one thing on the Tile that *is* a verdict, the lit edge. */
.break-status {
    font-weight: 600;
}

/* RFSA-107: the selected day's NAS sync result. Separated from the Breaks above by a rule rather
   than by spacing alone, because it answers a different question about the same day and a reader
   scanning the Tile has to see where one ends and the other begins. */
.tile-sync {
    margin-top: 0.5rem;
    padding-top: 0.45rem;
    border-top: 1px dashed var(--line);
    font-size: 0.9rem;
}

/* The whole summary line is the disclosure control, so it has to read as one — the default marker
   sits outside the line box and a bare <summary> renders list-item, which puts the fields on
   separate lines in some engines. */
.tile-sync summary {
    cursor: pointer;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.35rem;
    /* The Break row's own vertical padding, so the hover lift the two now share lands as the same
       band on both rather than as a thinner one here (acceptance criterion 8). */
    padding: 0.2rem 0;
    /* Both halves, for the reason .break-row gives one level up: `display: flex` already suppresses
       the native triangle in most engines, and WebKit needs its own pseudo-element told as well.
       Said explicitly rather than left to the flex side effect, so the marker rule this line now
       shares with .break-index is the only thing drawing one and no engine can render two. */
    list-style: none;
}

.tile-sync summary::-webkit-details-marker {
    display: none;
}

/* The marker and the hover lift this line used to declare for itself now come from the two rules
   .break-row shares them with, one level up — that is what acceptance criterion 8 asks for, and it
   is also what stops the two expanders drifting apart the way they already had: this one had the
   glyph but no pointer response, and an underline where the Break rows lift. */

.tile-sync .tile-empty {
    /* Matches the Breaks section's own empty state, so "no sync ran" and "no Breaks reported" read
       as the same kind of statement rather than as two different weights of news. */
    margin: 0;
}

.tile-sync-folders {
    min-width: 0;
    margin-top: 0.4rem;
    font-size: 0.85rem;
}

.tile-sync-folders th,
.tile-sync-folders td {
    padding: 0.2rem 0.5rem 0.2rem 0;
    border-bottom: 1px solid var(--line-2);
    white-space: nowrap;
}

/* The category names the folder's role, the path says which share actually got read. Stacked rather
   than run together on one line: a NAS UNC path is long enough to push every count off the right of
   a Tile, and the counts are the part being compared across folders. */
.sync-folder-category {
    display: block;
    font-weight: 600;
}

/* Both are Station-supplied text with no length bound worth relying on, and both are on their own
   line inside a table whose every other cell is nowrap — so both must wrap where they are rather
   than letting one bad path widen the Tile (acceptance criterion 9). What they break on differs
   because what they are differs: see each rule. */
.sync-folder-path,
.sync-folder-error {
    display: block;
    white-space: normal;
}

/* The note tier, and smaller: the path answers "which share", which matters once the reader has
   already decided this row is the interesting one. break-all because a UNC path has no spaces to
   break at, so the alternative to breaking mid-segment is not breaking at all. */
.sync-folder-path {
    color: var(--dim);
    font-size: 0.8rem;
    word-break: break-all;
}

/* Acceptance criterion 9: the most readable thing in its row. Not by being the largest — it sits at
   the table's own size — but by contrast and weight, in a cell whose Outcome is bold and red: the
   value tier at 12.98:1 against the Tile, at normal weight, beside a verdict at 600. That is
   exactly the division RFSA-111 made inside an alert panel, where the lit edge carries the verdict
   and the sentence carries the information, and it is the reason this text is no longer left to
   inherit the .bad cell around it — the reason a folder failed is prose, and prose set in the
   verdict's own colour and weight reads as more verdict rather than as the answer.

   break-word rather than the path's break-all: this is a sentence, so it has spaces to break at,
   and breaking a word mid-way is a last resort here instead of the only option. */
.sync-folder-error {
    color: var(--text);
    font-weight: 400;
    word-break: break-word;
}

.break-file-count,
.tile-breaks-head > span:last-child {
    /* Right-aligned so the count sits under its own header rather than drifting away from it in the
       widest column on the row. */
    text-align: right;
}

.break-file-count {
    color: var(--dim);
}

/* Acceptance criterion 7: several playlists can be open at once, and each has to read as belonging
   to its own Break. RFSA-106's indentation alone did not carry that — with two open, the second
   playlist is as indented under the first Break's rows as under its own, and the reader is left
   counting rows to work out which is which.

   So the playlist becomes a well: --surface-inset, the step down the ramp RFSA-111 defined for a
   thing sitting *inside* a card, tethered to its Break by a rule down its left. Down the ramp rather
   than up it, which was the first attempt and does not work: --break-row's hover lift is
   --surface-raised, so anything lighter than the card either collides with that or — half a step
   short of it — is too faint to see at all. Recessing instead leaves the whole lighter half of the
   ramp to the pointer, and reads as what it is: this Break, opened up.

   The three left lengths still add up to RFSA-106's 1.4rem of indent, so the playlist sits where it
   always did. --line for the tether, not --line-2: this line brackets the rows above it rather than
   dividing them, and at the width the ramp's own steps are worth here the quieter hairline
   disappears against the well. */
.break-files {
    margin-left: 0.5rem;
    padding: 0.25rem 0.5rem 0.4rem 0.775rem;
    border-left: 2px solid var(--line);
    border-radius: 0 var(--radius-inline) var(--radius-inline) 0;
    background: var(--surface-inset);
}

.break-file-list {
    min-width: 0;
    margin: 0.3rem 0 0.2rem 0;
    font-size: 0.8rem;
}

.break-file-list th,
.break-file-list td {
    padding: 0.2rem 0.45rem 0.2rem 0;
    white-space: nowrap;
}

/* The right padding on every cell is the gutter before the next column; on the last one it is a
   gutter before nothing, and this table has no width to spare — six columns inside a well inside a
   28rem Tile. The ~7px it gives back all goes to File, which is the only column that can use it,
   and is the difference between a typical name fitting on one line and breaking mid-token. */
.break-file-list th:last-child,
.break-file-list td:last-child {
    padding-right: 0;
}

/* RFSA-118: a label, like every other column header the RFSA-109 canvas draws and like
   .tile-fact-label two rows below it in the same Tile. This was the one header on the dashboard set
   in running text at the size of the values under it, which is what left the playlist reading as a
   paragraph of columns rather than as a table.

   Smaller here is free, unlike on .tile-breaks-head above: that header shares --break-columns with
   the rows it labels, and those track floors are in `ch`, so a font-size on the header resolves them
   differently from the rows and the two stop lining up. This table is `table-layout: auto` and its
   columns are computed from every cell at once, so the header can be any size it likes. --dim on
   --surface-inset measures 6.10:1. */
.break-file-list th {
    color: var(--dim);
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    border-bottom: 1px solid var(--line-2);
}

/* Monospaced, where this table was the one place on the dashboard machine values were not. A file
   name, a duration and a played stamp are exactly what .mono exists for — the Break row this
   playlist hangs off already sets its own stamps in it, so an operator reading down from a Break
   into its files was crossing a family change for no reason.

   --muted rather than --text, so the one cell that is worth reading in full can be the one that is:
   see .file-name. 9.51:1 on --surface-inset.

   The rules come back too. The global `th, td` rule draws them and this table used to cancel them;
   a six-column list of near-identical short values is precisely the shape that needs a reader's eye
   guided along a row, and the well it sits in is what keeps them from adding up to noise. */
.break-file-list td {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    color: var(--muted);
    border-bottom: 1px solid var(--line-2);
}

/* The last rule would draw the playlist's own bottom edge a few pixels above the well's, which reads
   as a table that has been cut off rather than as one that has ended. */
.break-file-list tr:last-child td {
    border-bottom: 0;
}

/* Role and State are words, not machine values, so they stay in the UI family — the same split the
   Break row above makes, where .break-status is a word and only the three stamps are monospaced.
   State is also the one emphasised cell here, and 600 in the monospace family would be synthesised:
   that family ships regular only, which SelfHostedFontTests pins and whose rationale is exactly this
   case. Role is addressed by position because it is the only unclassed word column, and the six
   columns are fixed by the thead directly above and by BreakFileDetailPageTests, which writes the
   whole row out. */
.break-file-list td:nth-child(4),
.break-file-list .file-state {
    font-family: var(--font-ui);
}

/* The one column that can be arbitrarily long and is worth reading in full — a file name is how an
   operator recognises the spot. Everything else on the row is a short fixed-shape value, so this is
   the only cell that may wrap, and the only one lifted back to the value tier now that the rest of
   the row sits a step below it. 16.78:1 on --surface-inset, the highest reading on any surface. */
.break-file-list .file-name {
    white-space: normal;
    word-break: break-word;
    max-width: 22ch;
    color: var(--text);
}

.break-file-list .file-state {
    font-weight: 600;
}

.break-files-empty {
    margin: 0.3rem 0 0.2rem 0;
    font-size: 0.8rem;
    color: var(--dim);
}

/* "Pushed, but no playlist detail arrived" is the Fleet Dashboard reporting a gap in its own data,
   exactly as .tile-unavailable is one level up — where "not pushed yet" is an ordinary state of a
   Break whose turn has not come, and stays plain muted text.

   Those two, plus that plain muted text, are the three readings acceptance criterion 10 requires to
   stay distinct, and RFSA-112 keeps all three: the amber marker is the cue and neither its colour
   nor its width changed. What did change is the surround — this one now sits inside .break-files'
   well, so the same marker has a grey rule outboard of it that .tile-unavailable does not. Left that
   way on purpose. The well is what buys criterion 7, hoisting this one notice out of it would
   un-tether the Break whose only content *is* this notice, and against a recessed surface the amber
   reads at more contrast than it did on the card, not less. Verified on a render rather than argued:
   the two are still the same reading at two scopes. */
.break-files-unavailable {
    border-left: 3px solid var(--warn);
    padding-left: 0.5rem;
    color: var(--warn);
}

/* Acceptance criterion 4: still visible, deliberately not prominent — the Breaks above are what the
   Tile is for, and these are the live facts that used to be the table's own columns. */
.tile-footer {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 1rem;
    margin-top: 0.6rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--line);
    font-size: 0.85rem;
}

/* Acceptance criterion 11: each fact reads as a label and then a value, with the label subordinate.
   The footer already groups the pairs — it is a flex row whose gaps put 1rem between facts and
   nothing between a label and its own value — so what was missing was the subordination: a label
   and a value both set in running text at the same size read as a sentence, and the eye has to
   parse it to find out which half is the reading.

   Still --muted, which is RFSA-111's label tier and exactly what this is; the subordination is
   carried by size and case instead, and dropping to --dim would have said "note" about something
   that is not one. Smaller and uppercased, letter-spaced back out because caps set tight read as a
   word rather than as a label. The two roughly cancel on width — caps are about 15% wider per glyph
   and this is 15% smaller — so a footer of six facts wraps where it wrapped before.

   The Tile's NAS sync line takes this class too, and gets the same treatment for the same reason:
   "NAS SYNC" in front of a verdict is a label, not the first two words of a sentence. */
.tile-fact-label {
    color: var(--muted);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    /* These are two- and three-word labels sized to sit on one line with their value; a label that
       wraps stops looking like the value's own. */
    white-space: nowrap;
}

.tile-fact-label::after {
    content: " ";
}

/* Both can carry arbitrarily long Station-supplied text, so they take the whole width rather than
   stretching the flex row they sit in. */
.tile-fact-event,
.tile-fact-error {
    flex-basis: 100%;
    word-break: break-word;
}

/* The Event's detail goes on its own line, which is what the fleet grid table's own
   `.event-cell > span` rule used to do before RFSA-104 replaced the table. Without it the age and
   the detail are adjacent inline elements with no whitespace between them in the rendered HTML —
   Razor strips it around the @if the detail sits in — so a real Station renders
   "7s agoBreak 6 on 2026-09-02: 2 items". Also keeps a long detail from pushing the age away from
   the Event type it belongs to. */
.tile-fact-event .event-detail {
    display: block;
}

/* Every page-level table on the dashboard is inside one of these, and the two together now read as
   one panel: the wrapper owns the outline and the corner, so the table itself needs neither and its
   own sideways scroll is clipped to the radius. */
.grid-wrapper {
    overflow-x: auto;
    border: 1px solid var(--line);
    border-radius: var(--radius-panel);
}

table {
    border-collapse: collapse;
    width: 100%;
    min-width: 52rem;
}

th, td {
    text-align: left;
    padding: 0.45rem 0.7rem;
    border-bottom: 1px solid var(--line-2);
    vertical-align: top;
}

th {
    font-weight: 600;
    white-space: nowrap;
    color: var(--muted);
}

/* RFSA-83: a sortable column header is a link, but it heads a column — it should read as a header
   an operator can click, not as prose with a hyperlink in it. The arrow (SortIndicator) marks
   which one is actually ordering the grid. `color: inherit` now also keeps it out of --accent,
   which is what a bare `a` on this page takes. */
th a {
    color: inherit;
    text-decoration: none;
}

th a:hover,
th a:focus {
    text-decoration: underline;
}

/* The card, and the raised band on it. Scoped to .grid-wrapper rather than declared on `table`,
   because the Tile's own two tables (.break-file-list, .tile-sync-folders) are nested inside a Tile
   and must not each grow a card of their own.

   --muted on the header, not --dim: --surface-raised is the lightest surface the dashboard has, and
   the note tier measured against it comes to 4.72:1 — clear, but with nothing spare. A column
   header is a label, so it takes the label tier, at 7.36:1. */
.grid-wrapper table {
    background: var(--surface-card);
}

.grid-wrapper th {
    background: var(--surface-raised);
}

/* The wrapper's own bottom edge already closes the table; a last-row rule under it reads as a
   stray empty row. */
.grid-wrapper tbody tr:last-child > * {
    border-bottom: 0;
}

.muted {
    color: var(--muted);
}

.ok {
    color: var(--ok);
    font-weight: 600;
}

.bad {
    color: var(--bad);
    font-weight: 600;
}

/* A page-level alert is a panel, not a coloured sentence. Keyed on role="alert" rather than on a
   new class, so it reaches all of them — the sign-in rejection, the password rejections, the
   invalid Password link, the reset confirmation — plus App.razor's unclassed "no access" and
   "nothing at this address", without touching any markup. Deliberately not a match for
   .credential-reveal, which carries role="alert" on a <div> and is a panel in its own right.

   Inside the panel the text returns to the value tier at normal weight: the panel and its lit edge
   are what say "read this", and a slab of bold coloured text on top of them says it twice. The
   inline .ok/.bad spans elsewhere — a revoked credential in a table, a settings error in a list —
   are untouched and keep their colour, because there they are the only cue there is. */
p[role="alert"] {
    margin: 0 0 1rem;
    padding: 0.6rem 0.8rem;
    border: 1px solid var(--line);
    border-left-width: 3px;
    border-radius: var(--radius-control);
    background: var(--surface-card);
}

p.ok[role="alert"],
p.bad[role="alert"] {
    color: var(--text);
    font-weight: 400;
}

p.ok[role="alert"] {
    border-left-color: var(--ok);
}

p.bad[role="alert"] {
    border-left-color: var(--bad);
}

/* RFSA-110: the utility that puts a machine-generated value in the monospace family — a timestamp,
   an age, a UNC path, a credential, a setting key, a file name, a count. No markup carries it yet:
   the columns it exists for are moved onto it by the later tickets of RFSA-109, which this one
   deliberately does not start, and it lands here now so each of those is a change of class and
   nothing else. (.credential-value below is already monospaced and takes --font-mono directly
   rather than this class — it is one block, not a column, so it has no use for tabular figures.)

   tabular-nums is the whole reason a column of stamps is worth moving: proportional digits let
   06:20:10 and 11:02:17 sit at different widths, so nothing lines up down the column and a digit
   that changed cannot be spotted without reading every row. Stated explicitly rather than left to
   IBM Plex Mono — which is monospaced and so already tabular — because the declaration is what
   keeps the utility aligning when it renders on the fallback stack instead.

   RFSA-112 is the first caller, and it joins this rule rather than restating it — see the Break
   stamp rule above for why, and for why it selects its cells by track position instead of carrying
   the class in markup. Anything added here reaches it. One constraint on the selector list, and it
   is not cosmetic: `.mono` must stay last, alone on its line and immediately before the brace,
   because SelfHostedFontTests reads this rule by exact selector and a `.mono,` followed by a comma
   is a rule it cannot find. Append below, never after `.mono`. */
.break-row > .break-cell:nth-child(n+4):nth-child(-n+6),
.mono {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}

/* An inline machine value inside prose — <code>HH:mm:ss</code> in a Settings hint, and the two
   credential blocks below, which are <code> too and override the box part of this. The inset well
   is what separates it from the sentence around it; the colour is inherited rather than set, so a
   value quoted inside a note stays in the note's own tier instead of jumping a tier above it. */
code {
    font-family: var(--font-mono);
    /* Barely down from the prose around it, and measured rather than guessed at: IBM Plex Mono's
       x-height sits below Archivo's, so the usual 0.9em step reads as a size smaller rather than as
       the same sentence in another family. */
    font-size: 0.95em;
    padding: 0.05em 0.3em;
    border-radius: var(--radius-inline);
    background: var(--surface-inset);
    color: inherit;
}

/* RFSA-83's derived health; RFSA-112's treatment of it. Still the point RFSA-83 made — a faulted
   Station has to be findable in a 44-Station fleet without reading any of the other Tiles — but now
   said in one place per Tile rather than two. The 10% body tint the two fault states carried is
   gone: a wash that faint across a whole card is a difference an operator has to hunt for, and it
   only ever existed for two of the four states, so a degraded Station was marked by an edge *and* a
   field while an on-air one was marked by an edge alone. All four now speak in the same place, at
   the same intensity, and that is what makes a column of Tiles scannable — the eye runs down the
   edges and nothing else moves.

   The three lit states are told apart by hue at one lightness and one chroma, which is RFSA-83's own
   ruling re-lit by RFSA-111: no verdict shouts louder than another because of how its hue happens to
   render. Hue is therefore never the only cue — .health-badge below carries the word beside it
   (acceptance criterion 3), because a colour-blind operator cannot read a status made of colour. */
.station-tile.health-onair {
    --tile-edge: var(--ok);
    --tile-glow: var(--ok);
}

.station-tile.health-degraded {
    --tile-edge: var(--warn);
    --tile-glow: var(--warn);
}

.station-tile.health-offline {
    --tile-edge: var(--bad);
    --tile-glow: var(--bad);
}

/* The two states acceptance criterion 1 rules carry no colour: an idle Station, and any Station on a
   past day (RFSA-104 AC2 — not a fifth health, the absence of one). The edge keeps its width so a
   Tile does not shift when the day changes, and takes the Tile's own hairline so nothing about it
   reads as a verdict. Both leave --tile-glow at its default, so an unlit Tile is unlit rather than
   faintly lit in grey; said explicitly rather than left to .station-tile's own defaults, because
   what these two states are is a decision and not an omission. */
.station-tile.health-idle,
.station-tile.health-historical {
    --tile-edge: var(--line);
    --tile-glow: transparent;
}

/* The health word itself. Nowrap so "on air" stays one token. Keyed on the health class rather than
   on being inside a tinted row, so the grid's cell and the drill-down's header badge — which is the
   same verdict rendered at a different scope — cannot end up two different colours.

   RFSA-113 acceptance criterion 9 makes it a chip. RFSA-112 wanted this and deferred it, because a
   tinted chip on the Tile would have landed beside the drill-down's bare online/offline spans and
   made the two scopes look like two different verdicts — which is exactly what this criterion
   forbids. Both scopes are in this ticket, so both change together.

   The fill and the border are color-mix over `currentColor`, not over a named verdict token, and
   that is what lets one rule serve every wearer. The four .health-* rules below still supply the
   colour; the drill-down's fallback spans, which have no derived health to name and so wear .ok or
   .bad instead, get the identical chip from their own colour. A verdict rendered at one scope
   cannot be a chip while the same verdict at another scope is a bare word.

   Percentages are low on purpose: at 14% the fill is a tint the verdict colour still reads at full
   contrast against, so the chip is a shape around the word rather than a second, weaker colour
   competing with it. */
.health-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.15rem 0.55rem 0.15rem 0.45rem;
    border: 1px solid color-mix(in srgb, currentColor 40%, transparent);
    border-radius: 999px;
    background: color-mix(in srgb, currentColor 14%, transparent);
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    white-space: nowrap;
}

/* RFSA-118 acceptance criterion 4: the same dot .fleet-counter-label carries, so a Tile and the
   count it is one of read as one verdict in one shape rather than as a chip and a cell that happen
   to share a hue. currentColor again, for the reason the fill and border above are — the four
   .health-* rules stay the only place a verdict's colour is decided.

   Nothing here touches the word's contrast: the dot is a sibling of the text, not a backdrop for
   it, so RFSA-115's four measurements against the 14% tint (5.80 offline / 5.93 degraded / 6.29 on
   air / 6.50 idle) are unchanged. Uppercasing is CSS, so the label strings the page tests assert on
   are untouched too.

   The two unlit states get no glow, matching .station-tile's own edge rules, which already light
   on air / degraded / offline and leave idle and historical at the plain hairline. A glow is what
   makes a verdict findable across ~44 Tiles without reading one, and idle is not a verdict to go
   and find — it is a Station with nothing wrong and nothing on. */
.health-badge::before {
    content: "";
    flex: none;
    width: 0.4rem;
    height: 0.4rem;
    border-radius: 50%;
    background: currentColor;
    box-shadow: 0 0 6px currentColor;
}

.health-badge.health-idle::before,
.health-badge.health-historical::before {
    box-shadow: none;
}

/* One rule per verdict, two wearers each. RFSA-114's masthead counters join the badge here rather
   than restating the mapping next to their own layout: a count and the Tiles it counts are the same
   verdict, and acceptance criterion 3 is only structurally true — rather than true until someone
   edits one of two lists — while there is a single place that says which colour that verdict is.
   Each wearer takes the hue as currentColor and spends it differently: the badge on a chip's fill
   and border, the counter on its dot. */
.health-badge.health-offline,
.fleet-counter.health-offline {
    color: var(--bad);
}

.health-badge.health-degraded,
.fleet-counter.health-degraded {
    color: var(--warn);
}

.health-badge.health-onair,
.fleet-counter.health-onair {
    color: var(--ok);
}

/* RFSA-115: the label tier, where this was the note tier. --dim was measured against a *surface*
   when the token block was written — 5.40:1 on --surface-card — but .health-badge above then puts
   the word on a 14% tint of its own colour, and for the three hued states that tint moves the
   backdrop sideways in hue while for this one it moves it straight up the same grey ramp the text
   is on. So the chip cost idle what it cost none of the others: the word measured 4.47:1 on the
   Tile, the one reading on the nine pages under 4.5:1. At --muted it is 6.50:1, which puts the four
   badges in one band (5.80 offline / 5.93 degraded / 6.29 on air / 6.50 idle) rather than three
   clear and one short.

   Still a neutral grey, so nothing about it reads as a verdict — that is what the two unlit states
   are about, and --muted is a tier, not a hue. It also corrects the counters strip, where this rule
   is the other wearer: an idle count set in --dim beside a --muted .fleet-counter-label was the one
   segment whose word outranked its own number, which is exactly the inversion .fleet-counter-zero
   below exists to avoid. */
.health-badge.health-idle,
.fleet-counter.health-idle {
    color: var(--muted);
}

/* Acceptance criterion 2's zero. The segment keeps its place and drops out of the lit tier rather
   than merely going grey: --surface-inset makes it read as a slot with nothing in it from one
   segment, where grey among grey is a difference an operator can only find by comparing two — the
   same ruling RFSA-113 made for an unavailable day bound.

   Below the four rules above and at equal specificity, which is the whole of how it takes the hue
   back off a segment nothing is in. Moving it above them would silently stop it working. */
.fleet-counter.fleet-counter-zero {
    color: var(--dim);
    background: var(--surface-inset);
}

/* RFSA-118: the hairline is no longer the segment's own border but the panel's internal rule, so a
   zero no longer lightens it — a divider that changed weight with the count would make the panel's
   own grid move. What says "empty" is the inset surface and the unlit tier, which is what it always
   was; the border was only ever agreeing with them. */

/* Both halves, where before this was the label alone. .fleet-counter-value now sets --text
   explicitly (the hue moved to the label), so without this a zero would keep a full-strength number
   over a dimmed word — the exact inversion this rule exists to prevent, arrived at from the other
   side. --dim on --surface-inset measures 6.10:1. */
.fleet-counter-zero .fleet-counter-value,
.fleet-counter-zero .fleet-counter-label {
    color: var(--dim);
}

/* And its dot goes out. Nothing lit means nothing to go and fix — a grey dot still burning beside a
   zero is the one thing on the panel that would read as a reading when it is an absence. */
.fleet-counter-zero .fleet-counter-label::before {
    background: var(--line-2);
}

/* The drill-down's Detail and Last error cells — free text a Station sent, so a sentence, a stack
   frame, or one 400-character UNC path with no space in it, and this rule is on both columns.

   RFSA-113 acceptance criterion 7: overflow-wrap: anywhere rather than the word-break: break-word
   this carried, and the difference is the whole fix rather than a modernisation. break-word only
   breaks a word once the line box is already too narrow for it — but these live in an auto-layout
   table, where a cell holding an unbreakable token asks for that token's full width as its minimum
   and the table simply grows to give it, past the max-width here and past .grid-wrapper, until the
   page scrolls sideways and every other column has been squeezed to pay for it. `anywhere` is the
   one value that also counts toward min-content width, so the column can shrink and the max-width
   is finally the constraint it always read as. */
.error-cell {
    max-width: 24rem;
    overflow-wrap: anywhere;
}

/* .event-cell and its span rule lived here until RFSA-104: they styled the fleet grid table's
   "Last event" cell, and the Tile's footer uses .tile-fact-event instead. Removed rather than left
   for a future reader to wonder which of the two is live. */

.event-type {
    font-weight: 600;
}

.event-detail {
    color: var(--dim);
    font-size: 0.85em;
}

h2 {
    font-size: 1.05rem;
    font-weight: 700;
    margin: 1.5rem 0 0.5rem;
}

button {
    font: inherit;
    padding: 0.3rem 0.8rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-control);
    background: var(--surface-raised);
    color: var(--text);
    cursor: pointer;
}

/* :not(.link-button) because that class is a <button> deliberately dressed as inline text (see its
   rule above), and any hover treatment more specific than it would put a raised box around "Sign
   out" in the masthead. */
button:not(.link-button):hover:enabled {
    border-color: color-mix(in srgb, var(--accent) 55%, var(--line));
    background: color-mix(in srgb, var(--accent) 14%, var(--surface-raised));
}

button:disabled {
    color: var(--dim);
    background: var(--surface-card);
    border-color: var(--line-2);
    cursor: not-allowed;
}

/* Form fields, declared once on the elements rather than per form: an inset well carrying the
   panel's own hairline, so a field reads as somewhere to type on a page where nothing else is
   inset. The <select> on the Dashboard users page had no rule at all before this and rendered its
   native chrome; `color-scheme: dark` is what makes the list it drops down match. */
input,
select,
textarea {
    font: inherit;
    padding: 0.3rem 0.5rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-control);
    background: var(--surface-inset);
    color: var(--text);
}

/* The note tier, so a placeholder is legibly not a value — 6.10:1 on the field's own surface. */
input::placeholder,
textarea::placeholder {
    color: var(--dim);
}

input:hover,
select:hover,
textarea:hover {
    border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
}

/* The two Admin forms that issue something — Register a Station, Invite a Dashboard user. RFSA-113
   acceptance criterion 12: one row of controls, which the flex default already gives — `stretch`
   makes the input, the <select> and the button resolve one height between them, so nothing needs
   declaring here to keep the row level. What did break the row is below.

   Not the auth pages' form, which is .auth-form and is deliberately the opposite shape: these two
   are an Admin issuing a credential from a toolbar, and those are a person signing in. */
.register-form {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
}

/* The row-breaker. `flex` rather than the flat `min-width: 22rem` this carried: the field still wants
   most of the row, but it can now give width back instead of forcing the button — and, on the invite
   form, the role <select> too — onto a second line the moment the viewport leaves it less than 22rem.
   That is what stopped this being one row of controls, and it did so at exactly the widths a laptop
   has. `min-width: 0` because a flex item's automatic minimum is its own content width, which would
   put the floor straight back. The box itself is still the `input` primitive above, so this form and
   the Settings one cannot drift. */
.register-form input {
    flex: 1 1 20rem;
    min-width: 0;
    max-width: 26rem;
}

/* Deliberately loud: this panel is the only time the plaintext credential is ever visible, and
   RFSA-113 acceptance criterion 10 asks for the loudest thing on its page. One rule serves both
   callers — Stations' Station credential and Dashboard users' Password link — because both are a
   write-once secret and an Admin should not have to work out that they are the same kind of thing
   twice. (Both pages also now render the panel in the same position, directly under the subtitle;
   that half of the criterion is markup, not style.)

   The glow is what the border alone was not: a 2px outline is the same weight of statement as the
   Tile's health edge and the table wrapper's own outline, so on a page already full of outlines it
   did not read as urgent. An outset shadow in --ok lifts the panel off --surface-page instead, and
   nothing else on any of these pages is lit. Outset here, unlike the Tile's inset edge, because
   this panel has nothing scrolling inside it to clip a shadow and no gutter to hide one in. */
.credential-reveal {
    border: 2px solid var(--ok);
    border-radius: var(--radius-panel);
    padding: 1rem 1.15rem;
    margin: 1rem 0 1.5rem;
    background: var(--surface-card);
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--ok) 25%, transparent),
                0 0.5rem 1.75rem color-mix(in srgb, var(--ok) 18%, transparent);
}

/* h2's own 1.5rem top margin would push the heading a line and a half down inside the panel it
   titles, and its 1.05rem is the size of a section heading on a page — this is the page's subject
   for as long as it is on screen. */
.credential-reveal h2 {
    margin-top: 0;
    font-size: 1.2rem;
}

/* RFSA-113 acceptance criterion 11 keeps the two properties that matter and states why. `user-select:
   all` is "selectable in one gesture" literally: one click selects the whole secret, which is what an
   Admin does with it, and a partial selection of a credential is worse than none. --font-mono is the
   family every machine value on the dashboard is in, and here it also means an operator re-typing a
   lost character can tell l from 1 and O from 0. Full width and centred padding, so the block reads
   as the thing to copy rather than as a phrase inside the paragraph above it. */
.credential-value {
    display: block;
    margin: 0.5rem 0 0.75rem;
    padding: 0.5rem 0.7rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-inline);
    /* The stack this rule carried inline (ui-monospace, "Cascadia Mono", Consolas, monospace) is
       now the tail of --font-mono, with the self-hosted family in front of it — so the one place
       that was already monospaced renders in the same family as everything RFSA-109 moves onto
       .mono next, rather than in whatever the operator's machine happens to offer. */
    font-family: var(--font-mono);
    /* Back to full size: this is a block a reader copies by hand if the copy fails, not an inline
       quotation inside a sentence, so it does not take the `code` rule's 0.92em step down. */
    font-size: 1em;
    word-break: break-all;
    user-select: all;
}

/* `display: none` is the whole mechanism that hides the error bar — the Blazor script only ever
   sets it visible, so a stylesheet that drops this line renders a permanent banner across the
   bottom of every page. It stays first in the rule so it cannot be lost while the rest is tuned.
   The rest is what it looks like once Blazor does show it: the raised surface rather than the
   #ffefc2 slab this was, an amber top edge because a dropped circuit is a caveat about the page
   rather than a fault at a Station, and a shadow dark enough to read as depth on a dark page. */
#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    padding: 0.6rem 1.25rem;
    background: var(--surface-raised);
    color: var(--text);
    border-top: 2px solid var(--warn);
    box-shadow: 0 -0.5rem 1.5rem rgb(0 0 0 / 0.55);
}

/* The drill-down's two history tables (RFSA-82) carry an absolute timestamp and its age in one
   cell. Stacked rather than side by side so neither pushes the other out of the column, and
   nowrap so a narrow viewport breaks the table, not a timestamp. */
.when-cell > span {
    display: block;
    white-space: nowrap;
}

/* RFSA-113 acceptance criterion 6 (drill-down): the age is subordinate to the stamp above it. It
   was --muted at full size, which is the label tier at the value tier's size — the same weight of
   reading as the timestamp itself, in a cell where only one of the two is the fact and the other is
   arithmetic on it. Down to the note tier and a step smaller, so the column scans as a column of
   stamps with a gloss under each.

   Not monospaced, deliberately, though the stamp above it now is: "37m ago" is a phrase, not a
   value with digits to line up, and setting it in the same family as the stamp would make one
   two-line block where the point is that the first line is the reading. */
.when-cell > .when-age {
    color: var(--dim);
    font-size: 0.85em;
}

/* RFSA-113 acceptance criterion 8 (drill-down): quiet. The note tier and a rule above it, so the
   retention statement is separated from the last table by something other than white space and is
   plainly the smallest voice on the page. The colour lives here rather than on a `muted` class in
   the markup — the two used to both apply and it was source order alone deciding which won. */
.footnote {
    margin-top: 1.5rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--line-2);
    color: var(--dim);
    font-size: 0.85em;
}

/* RFSA-105: the Fleet Breaks Report, in the day selector's row because that is the control deciding
   which day it covers. Styled as an action rather than as prose: it reads alongside Prev / Next /
   Today, and it does something rather than navigating somewhere.

   RFSA-113 acceptance criterion 3 finishes that thought. It sat *outside* .day-nav's outline and yet
   wore the same chip the day steps used to, so it read as a fifth day control that had fallen off
   the end of the group. It now takes the `button` primitive's own surface and hover, which is what
   every other thing on the dashboard that *does* something looks like, and an arrow says which kind
   of doing. The arrow is decorative — the link text says "Breaks report" and a visually-hidden span
   says where it opens — so it is generated content and reaches no reader twice.

   RFSA-117 changed which arrow, and why: see the rule on .day-report::before below. */
.day-report {
    padding: 0.25rem 0.6rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-control);
    background: var(--surface-raised);
    color: var(--text);
    text-decoration: none;
    font-weight: 600;
}

/* RFSA-117: a north-east arrow, not the download arrow this used to carry. "\2193" promised a file
   on disk. It stays decorative, so it is not the only thing saying so — .visually-hidden text in the
   anchor carries "opens in a new tab" to readers this glyph never reaches. */
.day-report::before {
    content: "\2197";
    margin-right: 0.35rem;
    color: var(--accent);
}

/* RFSA-117: text for assistive technology that no sighted reader sees. The clip-rect pattern rather
   than display:none or visibility:hidden, both of which remove the element from the accessibility
   tree along with the screen — which would defeat the entire point. Kept a general utility rather
   than folded into .day-report, since "the label says more than the visible text" is not specific to
   that one control. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.day-report:hover,
.day-report:focus {
    border-color: color-mix(in srgb, var(--accent) 55%, var(--line));
    background: color-mix(in srgb, var(--accent) 14%, var(--surface-raised));
    text-decoration: none;
}

/* RFSA-105: the Admin-only Settings page. One field per row rather than a dense grid — each of
   these three values needs its hint read before it is changed, since they decide the contents of a
   document that leaves the organisation. */
.settings-form {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    max-width: 42rem;
}

.settings-field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

.settings-field label {
    font-weight: 600;
}

/* As with .register-form input: the box comes from the `input` primitive, and only the width that
   is particular to a time-of-day or a count field stays here. */
.settings-field input {
    max-width: 12rem;
}

/* One rule, two names. .settings-hint is what the Settings fields already carried; .field-hint is
   the same thing said under a password field on Profile and Set password, where RFSA-113 acceptance
   criterion 18 needs the minimum-length rule readable before a rejection as well as after. Aliased
   rather than renamed so no existing markup moves for a hint that has not changed. */
.settings-hint,
.field-hint {
    color: var(--dim);
    font-size: 0.85rem;
}

/* RFSA-113 acceptance criterion 13, the confirmation half: "Saved." sits in this row, beside the
   button that caused it, rather than anywhere the eye has to go looking for it. */
.settings-actions {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

/* RFSA-113 acceptance criterion 13, the failure half. This was a bare <ul> of red bullets *below*
   the form — below the fold on a short window, and behind the Admin's eye, which is on the Save
   button they just pressed. It is now a panel above the form, in the same shape p[role="alert"]
   gives every other rejection on the dashboard: a card with a --bad edge down its left.

   The list keeps its bullets, because there can be three of these at once (one per field) and three
   sentences run together are three sentences an Admin has to separate for themselves. The <li>s keep
   their .bad colour rather than dropping to the value tier the way an alert paragraph's text does —
   inside a list, colour is what pairs each line with the edge, and there is no single sentence for
   the edge to be the verdict of. */
.settings-errors {
    margin: 0 0 1rem;
    padding: 0.6rem 0.8rem 0.6rem 2rem;
    border: 1px solid var(--line);
    border-left: 3px solid var(--bad);
    border-radius: var(--radius-control);
    background: var(--surface-card);
    max-width: 42rem;
}

/* RFSA-113 acceptance criterion 15: the three unauthenticated pages are a centred card. They are the
   one part of the dashboard MainLayout's nav never reaches — it renders inside <Authorized> — so
   until now they were a bare h1 and a form floating at the top-left of a near-black page, with
   nothing on them naming the product a person was about to hand a password to.

   max-width in rem rather than a percentage: this card holds one column of fields, and its right
   measure is set by how wide a password field should be, not by the viewport. `margin: 4rem auto`
   centres it horizontally and drops it clear of the top edge; deliberately not vertically centred,
   which would move the form every time a rejection panel appeared above it. */
.auth-card {
    max-width: 26rem;
    margin: 4rem auto;
    padding: 1.75rem;
    border: 1px solid var(--line);
    /* Larger than --radius-panel, and the one corner on the sheet that is off the scale: this is
       the only element that is the whole page rather than a component on one, and at 26rem wide the
       panel radius reads as a tighter corner than it does on a Tile a third that size. 1.1rem is the
       canvas's own value for this card. */
    border-radius: 1.1rem;
    background: var(--surface-card);
    box-shadow: 0 1rem 2.5rem rgb(0 0 0 / 0.45);
}

/* The product's identity. A <p>, not an <h1>: the page's own h1 says what this screen is for ("Sign
   in with email and password"), and two h1s would make the brand compete with it for the one thing
   FocusOnNavigate lands on. It is also the reason it cannot be a heading — SetPasswordIntegrationTests
   asserts an unauthenticated page does *not* contain "Fleet Dashboard</h1>", which is how it tells a
   signed-out page from the dashboard itself. */
.auth-brand {
    margin: 0 0 1.25rem;
    color: var(--accent);
    font-weight: 700;
    font-size: 0.8rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

/* An alert inside the card cannot be --surface-card, which is the card. One step up the ramp, so a
   rejection reads as a panel on the card rather than as a stripe of colour with a border around
   nothing. */
.auth-card p[role="alert"] {
    background: var(--surface-raised);
}

/* h1's own 0.25rem is the gap before a .subtitle, which is the sentence belonging to the heading. On
   these pages the next thing is usually a rejection panel or a form, neither of which belongs to it. */
.auth-card h1 {
    margin-bottom: 1rem;
}

/* Whatever ends the card ends it flush with the padding. The last element is a different one on each
   of the four pages — an .auth-alt, a hint, or (on an invalid Password link, where the explanation is
   the whole content) the alert panel itself — and each carries a bottom margin of its own that would
   otherwise show as a different amount of dead space under each. */
.auth-card > :last-child {
    margin-bottom: 0;
}

/* RFSA-113 acceptance criterion 16: a single column with a full-width submit — the shape a person
   signing in expects, and the opposite of .register-form's toolbar. Also worn by Profile's password
   form (criterion 14), which is the same job on a page that does have the nav: max-width is what
   keeps it a column there, where there is no card to constrain it. */
.auth-form {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    max-width: 26rem;
}

/* box-sizing because the `input` primitive has padding and a border, and 100% of the column plus
   0.5rem of padding each side overflows the card it is in. Declared here rather than globally: this
   is the only place on the dashboard where a field is asked to fill its container exactly. */
.auth-form input,
.auth-form button {
    width: 100%;
    box-sizing: border-box;
}

.auth-form button {
    margin-top: 0.4rem;
    padding: 0.45rem 0.8rem;
    font-weight: 600;
}

/* The line under an auth form carrying the other ways in — Google instead, forgot your password,
   back to sign in. The note tier, with the links still in --accent, so the sentence is quiet and the
   two things in it that are clickable are not. */
.auth-alt {
    margin: 1rem 0 0;
    color: var(--dim);
    font-size: 0.85rem;
}
