/*
 * The dashboard's only stylesheet. Hand-written; the project has no node toolchain.
 *
 * Two rules:
 *
 * 1. Colour is decoration. Every state is spelled out in a word and a glyph in the markup, and
 *    every SVG cell is filled with a pattern as well as a colour, so nothing is lost to red/green
 *    colour blindness or a monochrome printout.
 * 2. Text meets WCAG AA on its background. Every colour below has a contrast ratio of at least
 *    4.5:1 against its own scheme's surface for text, and at least 3:1 for cell fills and borders,
 *    the AA threshold for graphical objects.
 *
 * There are two schemes, selected by `prefers-color-scheme` only; there is no toggle. Both palettes
 * are in the two `:root` rules below. Every other rule names a token, never a colour, so the dark
 * scheme is about thirty lines. The tests enforce this.
 *
 * The /changes page reuses .badge, .tile, .block and the `st-` / `pf-` colours. Its own section
 * adds only the log layout and the three `src-` source colours, which name a subsystem, not a
 * state.
 *
 * Class prefixes are written as `st-` and not as wildcard selectors, because a wildcard followed by
 * a slash closes this comment. That once ended this banner early: the remaining prose ran into the
 * `:root` rule, the whole palette was dropped as an invalid rule, and every `var()` on every page
 * resolved to nothing.
 */

:root {
    /* What the user agent paints: the form controls on /changes, scrollbars and the canvas.
       Without this, the <select> stays white with black text on a dark page, and no author rule
       can reach inside it. It also enables the dark block below in browsers that would otherwise
       assume every page is light. */
    color-scheme: light dark;

    --ink: #1a1a1a;
    --muted: #4a4a4a;
    /* 3:1 against both surfaces below is the AA threshold for a graphical object. Measured:
       #8a8a8a is 3.45:1 on white and 3.17:1 on the alternate surface; #5c5c5c, for the edge of a
       whole box, is 6.6:1 and 6.1:1. Neutral greys, because a tinted border next to a status
       badge would read as a fifth state. */
    --rule: #8a8a8a;
    --rule-strong: #5c5c5c;
    --surface: #ffffff;
    --surface-alt: #f5f5f5;
    /* The interactive colour: 6.3:1 on white, and not in the status palette. Everything
       clickable uses it; nothing else does. */
    --action: #0b57d0;
    --action-surface: #eaf1fd;

    /* 4.5:1 or better against white. Not distinct from each other: measured in greyscale they
       separate by 1.00:1 to 1.46:1, and #b3261e and #8a5000 are identical to a monochrome
       printer. This is why layout.html's <defs> block exists: texture distinguishes states, hue
       repeats it, and lightness is used for contrast against the page. */
    --not-available: #b3261e;
    --unknown: #8a5000;
    --available: #1b5e20;
    --no-data: #595959;

    --ambiguous: #5b2d8e;
    --suspect: #8a5000;
    --matched: #1b5e20;
    --unmatched: #595959;

    --added: #1b5e20;
    --removed: #b3261e;
    --added-surface: #eaf5ea;
    --removed-surface: #fbeceb;

    /* The two colours of each timeline pattern: its ground and the mark drawn over it. They are
       here, outside the <defs>, because they must change with the scheme; a #f2f2f2 ground would
       be a white square on a dark page. The four textured states take their ground from the
       state colour above and only their mark from here. `noData` has both, because its ground is
       the near-invisible one that lets the dot carry the cell. */
    --not-available-mark: #ffdad6;
    --unknown-mark: #ffe0b2;
    --ambiguous-mark: #e6d6f5;
    --no-data-ground: #f2f2f2;
    --no-data-mark: #767676;
    /* Used only by the evidence bar. On a timeline `available` is the one solid fill, which
       makes a textured cell read as "something happened here". The evidence bar has no
       uneventful state: its three fresh bands are an ordered scale and all three must be
       distinguishable. 6.3:1 on #1b5e20. */
    --available-mark: #c8e6c9;

    /* The hairline around a day cell. Ink at quarter strength, so it reads as an edge on every
       fill it crosses. */
    --cell-edge: rgba(0, 0, 0, 0.25);

    /* The change log's fourth source accent. A dedicated colour because the .src-* accents name
       a subsystem; a status source painted --not-available would read as "every status change is
       an outage". 6.9:1 on white, and distinguishable in greyscale from the other three accents. */
    --source-status: #00636f;
}

/* The dark scheme.
 *
 * Every token above is redefined here and none is added; the tests enforce this. A token defined
 * only above would leave a light colour on a dark page with no error, and only an assertion catches
 * that.
 *
 * States are inverted: on white they are dark hues at 4.5:1, so on a dark surface they are light
 * hues. Dimming the same hues would give a 1.5:1 maroon on near-black. Measured against --surface
 * (#15181d): #f2938c 7.90:1, #e2a75c 8.40:1, #7cc47f 8.52:1, #a9b0ba 8.14:1, #c2a4ee 8.34:1, and
 * each is at least 7:1 on --surface-alt too, since a badge sits on either. Pattern marks invert
 * with them: a textured cell's ground is now the light colour, so its mark is the dark one.
 *
 * #15181d and not #000, because cells, rules and marks need room below the text in lightness, and
 * maximum-contrast white-on-black smears for astigmatic readers.
 *
 * `screen and` handles printing. `prefers-color-scheme` reports `dark` while printing too. Without
 * the qualifier, a reader who prefers dark would print #e6e8ec ink on white paper, and the @media
 * print block below would have to restate every token to undo it. So the dark palette is
 * screen-only and paper uses the light palette, which was measured against white.
 */
@media screen and (prefers-color-scheme: dark) {
    :root {
        --ink: #e6e8ec;
        --muted: #b6bcc6;
        /* 4.29:1 and 3.84:1 for --rule on the two surfaces, 7.60:1 and 6.81:1 for --rule-strong:
           the same 3:1 graphical-object threshold as the light values, and neutral for the same
           reason. */
        --rule: #767d88;
        --rule-strong: #a3aab4;
        --surface: #15181d;
        --surface-alt: #1e222a;
        --action: #9cc0fb;
        --action-surface: #17233a;

        --not-available: #f2938c;
        --unknown: #e2a75c;
        --available: #7cc47f;
        --no-data: #a9b0ba;

        --ambiguous: #c2a4ee;
        --suspect: #e2a75c;
        --matched: #7cc47f;
        --unmatched: #a9b0ba;

        --added: #7cc47f;
        --removed: #f2938c;
        --added-surface: #16261a;
        --removed-surface: #2a1614;

        --not-available-mark: #5c1512;
        --unknown-mark: #3d2a0a;
        --ambiguous-mark: #2e1a4d;
        --no-data-ground: #262a32;
        --no-data-mark: #a9b0ba;
        /* 6.1:1 on #7cc47f. Dark on a light ground here, light on a dark one above: a mark is
           always the opposite of its ground, so the <defs> geometry carries no colour. */
        --available-mark: #123f16;

        --cell-edge: rgba(255, 255, 255, 0.28);

        --source-status: #5fc7d6;
    }
}

* {
    box-sizing: border-box;
}

/* `hidden` is how the page handles no-JavaScript: the lift row ships a visible link and a hidden
   button, and dashboard.js swaps them. The user agent's `[hidden] { display: none }` loses to any
   author rule that sets `display`; `button.detail-toggle { display: inline-flex }` once showed a
   dead button on all 446 rows before any script ran.

   Stated once here, so it covers every element that gets both a `display` and `hidden`.
   `!important` is needed: without it this 0-1-0 selector near the top of the file loses to every
   later or more specific `display`. Nothing on this site is shown by CSS while `hidden`; the
   script shows an element by removing the attribute. */
[hidden] {
    display: none !important;
}

body {
    margin: 0;
    padding: 0;
    color: var(--ink);
    background: var(--surface);
    font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

/* Visible focus everywhere. The expandable rows must be usable by keyboard alone.

   Every hover rule below has a :focus-visible twin next to it, because keyboard users never
   hover. Add new rules to both lists. */
:focus-visible {
    outline: 3px solid var(--action);
    outline-offset: 2px;
}

a {
    color: var(--action);
}

a:hover,
a:focus-visible {
    background: var(--action-surface);
    text-decoration: underline;
}

/* Interactive elements are drawn as controls: a border, their own surface, and the action
   colour. Plain text has none of the three. */
button,
input,
select {
    font: inherit;
    color: var(--ink);
    background: var(--surface);
    border: 1px solid var(--rule-strong);
    border-radius: 4px;
    padding: 0.15rem 0.5rem;
}

button {
    color: var(--action);
    cursor: pointer;
}

button:hover,
button:focus-visible {
    background: var(--action-surface);
    border-color: var(--action);
}

input:hover,
select:hover,
input:focus-visible,
select:focus-visible {
    border-color: var(--action);
}

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* Skip link: hidden until focused. */
.skip {
    position: absolute;
    left: -9999px;
    top: 0;
    background: var(--surface);
    padding: 0.5rem 1rem;
    z-index: 10;
}

.skip:focus {
    left: 0;
}

/* The patterns take no space and are only referenced through `url(#p-…)`. Zero-sized, not
   `display: none`: the patterns take their colours from this stylesheet, so their children must
   keep computing styles. `display: none` removes the subtree from the render tree; the reference
   would still resolve but `fill` and `currentColor` would not, and every textured cell would
   render black. */
svg.defs {
    position: absolute;
    width: 0;
    height: 0;
}

header.site,
footer.site {
    padding: 0.75rem 1.5rem;
    border-bottom: 1px solid var(--rule);
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    align-items: baseline;
}

footer.site {
    border: 0;
    border-top: 1px solid var(--rule);
    color: var(--muted);
    font-size: 0.85rem;
}

header.site h1 {
    font-size: 1.25rem;
    margin: 0;
}

header.site h1 a {
    color: inherit;
    text-decoration: none;
}

/* The organisation and the page's subject sit beside the product name in a lighter weight, as in
   the EPIAP inspector, so the two tools look related. */
header.site h1 .org {
    color: var(--muted);
    font-weight: 500;
}

header.site nav ul {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    margin: 0;
    padding: 0;
}

/* The page nav takes the free width of its line, which puts the endpoints and the language
   switcher flush right. This holds when the header wraps, which the Dutch labels cause at
   ordinary desktop widths: free space goes to growing items before auto margins, so on the first
   line the switcher's `margin-left: auto` is zero and it sits against the endpoints, and on a
   line of its own it takes the whole width and stays at the right edge. */
header.site nav.sections {
    flex: 1 1 auto;
}

/* Machine endpoints, separated from the pages by a rule and by each link's format tag, so a link
   that returns XML looks different from one that opens a page. */
header.site nav.endpoints {
    margin-left: auto;
    padding-left: 1rem;
    border-left: 1px solid var(--rule);
}

.endpoints .format {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.7rem;
    letter-spacing: 0.03em;
    border: 1px solid var(--rule);
    border-radius: 3px;
    padding: 0 0.25rem;
    color: var(--muted);
}

/* The language switcher: a two-segment pill, last in the header. Same block, sizes and states as
   the EPIAP inspector's, with tokens mapped onto this palette, so both tools have the same language
   control.

   Three functional additions. `background: none` on the links, because the generic `a:hover` /
   `a:focus-visible` rules above give every link the action surface, which on a pill segment
   reads as a third state. `margin-left: auto`, which keeps the pill at the right edge when the
   header wraps it onto its own line. The end segments round their own outer corners; clipping
   them with `overflow: hidden` on the pill also cut three sides off a focused segment's ring
   (drawn outside the pill by the 2px offset), leaving a single vertical line. */
.lang {
    display: flex;
    margin-left: auto;
    border: 1px solid var(--rule);
    border-radius: 4px;
}

.lang a:first-child {
    border-radius: 3px 0 0 3px;
}

.lang a:last-child {
    border-radius: 0 3px 3px 0;
}

.lang a {
    padding: 2px 8px;
    font-size: 0.8rem;
    font-weight: 600;
    line-height: 1.6;
    text-decoration: none;
    color: var(--muted);
    background: none;
}

.lang a + a {
    border-left: 1px solid var(--rule);
}

.lang a:hover {
    color: var(--ink);
}

.lang a[aria-current="true"] {
    background: var(--action);
    color: var(--surface);
}

.lang a:focus-visible {
    outline: 2px solid var(--action);
    outline-offset: 2px;
}

main {
    padding: 1.5rem;
    max-width: 90rem;
    margin: 0 auto;
}

h2 {
    font-size: 1.1rem;
    border-bottom: 2px solid var(--rule);
    padding-bottom: 0.25rem;
}

h3, h4, h5 {
    font-size: 1rem;
    margin: 0.75rem 0 0.25rem;
}

/* ------------------------------------------------------------------ summary tiles */

.tiles {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    list-style: none;
    padding: 0;
    margin: 0 0 1rem;
}

.tile {
    border: 1px solid var(--rule-strong);
    border-left: 6px solid var(--rule-strong);
    border-radius: 4px;
    padding: 0.5rem 0.75rem;
    min-width: 12rem;
    background: var(--surface);
}

.tile .count {
    font-size: 1.6rem;
    font-weight: 700;
    display: block;
}

.tile .label {
    color: var(--muted);
}

.tile .glyph {
    font-weight: 700;
    float: right;
}

.tile.st-notAvailable { border-left-color: var(--not-available); }
.tile.st-unknown      { border-left-color: var(--unknown); }
.tile.st-available    { border-left-color: var(--available); }
.tile.st-noData       { border-left-color: var(--no-data); }

/* The status tiles span the row, like everything below them.

   `flex-basis: auto`: the basis is each tile's content, so `unknown` keeps the extra width its
   three reason lines need and the free space is shared equally on top. A fixed basis would make
   all four the same width and wrap those lines onto three rows each. */
.summary .tiles.statuses .tile {
    flex: 1 1 auto;
}

.tile .reasons {
    list-style: none;
    padding: 0;
    margin: 0.5rem 0 0;
    border-top: 1px solid var(--rule);
    font-size: 0.85rem;
}

.tile .reasons .count {
    display: inline;
    font-size: 1rem;
}

/* Three across: the two live components share the first box, then the export, then OSM. A
   column, so the age meter can be pinned to the bottom and the three boxes' meters line up. */
.facts .tile {
    min-width: 22rem;
    flex: 1 1 22rem;
    display: flex;
    flex-direction: column;
}

/* One live component inside the shared box. Equal shares of the height, so the two meters sit at
   the middle and the foot. */
.facts .pane {
    display: flex;
    flex-direction: column;
    flex: 1;
}

.facts .pane + .pane {
    margin-top: 1rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--rule);
}

/* The state belongs to what the heading names, so it is on the heading's line. This also keeps
   two stacked panes from looking like four headings. */
.facts .tile h3 {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.25rem 0.75rem;
}

.facts .tile h3 .state {
    font-weight: 400;
    color: var(--muted);
}

/* Inside a heading the glyph is part of the phrase and stays next to the word it qualifies, so
   the tile's corner float does not apply. */
.facts .tile h3 .glyph {
    float: none;
}

/* Red when either live component is wrong. Each heading already states its own state with a
   glyph and a word; this makes the box visible from the top of the page. */
.tile.liveness[data-liveness="degraded"] {
    border-left-color: var(--not-available);
}

/* A `dl` of `auto 1fr` gives all spare width to the longest term, which on the OSM tile is
   "Elevators in the OSM index", leaving its numbers too narrow. Cap the terms and let long
   values wrap; an export file name has no spaces and would otherwise overflow the tile border. */
.facts .tile dl {
    grid-template-columns: minmax(0, max-content) minmax(7rem, 1fr);
}

.facts .tile dd {
    overflow-wrap: anywhere;
}

/* What the feed has produced, below its state. Separated by a rule: the counts answer a
   different question from the liveness rows but belong to the same component, and a second `h4`
   here would be a fourth heading in a box that already has two. */
.facts .pane .counts-since {
    margin: 0.6rem 0 0;
    padding-top: 0.6rem;
    border-top: 1px dotted var(--rule);
    font-size: 0.8rem;
    color: var(--muted);
}

.facts .pane dl.counts {
    margin-top: 0.6rem;
    padding-top: 0.6rem;
    border-top: 1px dotted var(--rule);
}

/* The rule belongs to the group and is drawn once, so the caption takes it when present. Without
   this the list draws a second line directly under the caption's. */
.facts .pane .counts-since + dl.counts {
    margin-top: 0.15rem;
    padding-top: 0;
    border-top: none;
}

/* The qualification on a count whose meaning is not in the number. Muted and smaller because it
   describes the value. On a narrow tile it wraps under the value, where `dd` already has
   `overflow-wrap`. */
.facts .pane .caveat {
    display: block;
    font-size: 0.8rem;
    color: var(--muted);
}

/* Distinguishes a feed that never delivered anything from one that went quiet. It uses the
   `unknown` colour: it reports that we do not know the feed works, which is what `unknown` means
   elsewhere on this page. */
.facts .pane .never {
    color: var(--unknown);
    font-weight: 700;
}

dl {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.15rem 0.75rem;
    margin: 0.25rem 0;
}

dt {
    color: var(--muted);
}

dd {
    margin: 0;
}

.coverage .delta {
    font-weight: 700;
}

.warn {
    color: var(--not-available);
}

/* ------------------------------------------------------------------ badges */

.badge {
    display: inline-flex;
    align-items: baseline;
    gap: 0.3rem;
    border: 1px solid var(--rule);
    border-radius: 999px;
    padding: 0.05rem 0.6rem;
    white-space: nowrap;
}

.badge .glyph {
    font-weight: 700;
}

.badge.st-notAvailable { border-color: var(--not-available); color: var(--not-available); }
.badge.st-unknown      { border-color: var(--unknown); color: var(--unknown); }
.badge.st-available    { border-color: var(--available); color: var(--available); }
.badge.st-noData       { border-color: var(--no-data); color: var(--no-data); }

.badge.pf-ambiguous { border-color: var(--ambiguous); color: var(--ambiguous); }
.badge.pf-suspect   { border-color: var(--suspect); color: var(--suspect); }
.badge.pf-matched   { border-color: var(--matched); color: var(--matched); }
.badge.pf-unmatched { border-color: var(--unmatched); color: var(--unmatched); }

/* ------------------------------------------------------------------ stations */

details.station {
    border: 1px solid var(--rule-strong);
    border-radius: 4px;
    margin-bottom: 0.75rem;
    /* No `overflow: hidden` to clip the header background to the corner: the focus ring is drawn
       2px outside the summary, which sits on the block's edge, so hiding the overflow would hide
       the focus ring of this page's main control. The summary rounds its own corners. */
}

details.station > summary {
    cursor: pointer;
    padding: 0.5rem 0.75rem;
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    align-items: baseline;
    background: var(--surface-alt);
    border-radius: 3px 3px 0 0;
}

details.station:not([open]) > summary {
    border-radius: 3px;
}

/* The header is a control and responds to pointer and keyboard alike. An open block keeps a rule
   under its header so the header does not look like the first row. */
details.station > summary:hover,
details.station > summary:focus-visible {
    background: var(--action-surface);
}

details.station[open] > summary {
    border-bottom: 1px solid var(--rule-strong);
}

/* Every other disclosure on the site: element tags, an EPIAP diff, the stored XML. These
   summaries keep the browser's marker (nothing sets `display` on them, which is how the station
   header lost it) and use the action colour so they read as something that opens. */
details:not(.station) > summary {
    cursor: pointer;
    color: var(--action);
    padding: 0.2rem 0;
    width: fit-content;
}

details:not(.station) > summary:hover,
details:not(.station) > summary:focus-visible {
    background: var(--action-surface);
}

/* The browser's disclosure triangle comes from `display: list-item` on the summary, and the flex
   layout above replaces it, which left 171 station blocks with no open indicator. The marker is
   drawn here because the header is a flex row and a list marker is not a flex item.

   Rotated, because generated `content` text can end up in the accessible name, and the summary
   already announces its expanded state. */
details.station > summary::before {
    content: "";
    flex: 0 0 auto;
    align-self: center;
    width: 0.45rem;
    height: 0.45rem;
    border-right: 2px solid var(--ink);
    border-bottom: 2px solid var(--ink);
    transform: rotate(-45deg);
}

details.station[open] > summary::before {
    transform: rotate(45deg);
}

details.station > summary .name {
    font-weight: 700;
}

details.station > summary .code {
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

.badges {
    display: flex;
    gap: 0.4rem;
    flex-wrap: wrap;
    margin-left: auto;
}

/* `/` draws one table per station, 171 of them, each in its own disclosure; one merged grid would
   lose the per-station disclosure, and 446 rows do not fit on a screen. With auto layout each table
   sized its columns to its own content, so Amsterdam Centraal's long descriptions and a two-lift
   halt's short ones put the same five columns in different places, and nothing aligned down the
   page.

   Fixed layout makes the geometry a property of the stylesheet: the widths come from the <colgroup>
   the lift fragment ships, so all 171 tables, and /stations/{code}, which uses the same fragment,
   compute identical columns.

   The station header is not on this grid. It sits in the <summary>, outside the table, and is a
   control with a name, a code and badge counts, different from a lift row. Aligning it would
   squeeze the station name into the 9rem the lift codes need and make the header look like a
   first row. */
table.lifts {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
}

/* The history column is sized from its content: two SVG lines of 30 five-pixel day cells, 151px,
   plus cell padding. It is the most-scanned column and must not be squeezed; the cells have a
   `width` attribute and no `max-width`, so a narrower column would make them overflow into the
   next one. Description is the flexible column and wraps. */
col.lift-code        { width: 9rem; }
col.lift-description { width: auto; }
col.lift-status      { width: 16rem; }
col.lift-history     { width: 12rem; }
col.lift-detail      { width: 6rem; }

/* Fixed layout truncates where auto layout would grow, and an unreadable description makes a
   lift unidentifiable. So it wraps, mid-word if needed for long unbroken strings, with no
   ellipsis. */
td.description {
    overflow-wrap: anywhere;
}

/* Badges stay on one line elsewhere; inside a fixed-width column they may wrap, so a long reason
   does not overflow into the timeline. */
table.lifts .badge {
    white-space: normal;
    flex-wrap: wrap;
}

table.lifts caption {
    text-align: left;
}

table.lifts th,
table.lifts td {
    text-align: left;
    padding: 0.35rem 0.75rem;
    vertical-align: top;
    border-bottom: 1px solid var(--rule);
}

table.lifts thead th {
    font-size: 0.8rem;
    color: var(--muted);
    border-bottom: 2px solid var(--rule-strong);
    background: var(--surface-alt);
}

/* A lift is one row, so the line under it separates two lifts. Highlighted on hover and
   focus-within, so a keyboard user tabbing to the row's button sees which lift it opens. */
tbody.lift:hover,
tbody.lift:focus-within {
    background: var(--surface-alt);
}

tbody.lift .code a {
    font-variant-numeric: tabular-nums;
}

/* The panel visually belongs to the row above: inset on the action colour, on its own surface,
   so its blocks sit on white and the panel edge is clear. */
tr.detail-row > td {
    padding-top: 0.5rem;
    background: var(--surface-alt);
    border-left: 4px solid var(--action);
}

/* The lift's disclosure, in its own row. The chevron has the same shape and rotation as the
   station's. */
td.detail {
    white-space: nowrap;
}

button.detail-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    font-size: 0.9rem;
    padding: 0.1rem 0.5rem;
}

button.detail-toggle .chevron {
    width: 0.4rem;
    height: 0.4rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(-45deg);
}

button.detail-toggle[aria-expanded="true"] .chevron {
    transform: rotate(45deg);
}

/* ------------------------------------------------------------------ timelines */

svg.timeline {
    display: block;
    /* Spacing between the two lines is on the lines, so they stay aligned over the same day
       columns. */
    margin-bottom: 2px;
}

svg.timeline .cell {
    stroke: var(--cell-edge);
    stroke-width: 0.5;
}

/* Colour and pattern together. The pattern works for colour-blind readers, greyscale printouts and
   poor projectors.

   `available` and `matched` are the only solid fills on each line. They are the uneventful
   states, so a textured cell reads as "something happened here". Every other state has a
   texture, so no two states a reader must distinguish differ by hue alone. */
.c-notAvailable { fill: url(#p-notAvailable); }
.c-available    { fill: var(--available); }
.c-unknown      { fill: url(#p-unknown); }
.c-noData       { fill: url(#p-noData); }

.c-matched      { fill: var(--matched); }
.c-ambiguous    { fill: url(#p-ambiguous); }
.c-suspect      { fill: url(#p-suspect); }
.c-unmatched    { fill: url(#p-unmatched); }

/* The colours of the patterns those cells fill from. The geometry is in layout.html's <defs>; the
   colours are here because they must change with the scheme.

   Each pattern is a ground and a mark. The ground is a `.ground` rect filled by rule. The mark
   is the texture shape (line, circle, cross, band) and paints from `currentColor`, so this file
   does not need to know whether a mark is stroked or filled. That is why `color` is set on the
   pattern, and `stroke` and `fill` are not set on six element types. */
#p-notAvailable,
#p-notAvailable-hour { color: var(--not-available-mark); }
#p-notAvailable .ground,
#p-notAvailable-hour .ground { fill: var(--not-available); }

#p-unknown,
#p-suspect,
#p-unknown-hour,
#p-suspect-hour { color: var(--unknown-mark); }
#p-unknown .ground,
#p-suspect .ground,
#p-unknown-hour .ground,
#p-suspect-hour .ground { fill: var(--unknown); }

#p-ambiguous,
#p-ambiguous-hour { color: var(--ambiguous-mark); }
#p-ambiguous .ground,
#p-ambiguous-hour .ground { fill: var(--ambiguous); }

/* The one pair whose ground is not its state colour: `noData` is the absence of a reading, drawn
   as a near-empty cell populated only by the mark. */
#p-noData,
#p-unmatched,
#p-noData-hour,
#p-unmatched-hour { color: var(--no-data-mark); }
#p-noData .ground,
#p-unmatched .ground,
#p-noData-hour .ground,
#p-unmatched-hour .ground { fill: var(--no-data-ground); }

svg.timeline .tick {
    fill: var(--ink);
}

/* ------------------------------------------------------------------ the fleet band */

/* Counts over the same days as the lines below, at the same pitch and width. It fills from the
   `c-` rules above, so a state looks the same for one lift's day and for the fleet's count. */
figure.fleet-band {
    margin: 0 0 1rem;
}

figure.fleet-band figcaption {
    color: var(--muted);
    margin-bottom: 0.5rem;
}

/* The two lines side by side where there is room. Each is a 151-unit strip plus a short
   sentence, so full-width rows were mostly empty and cost a third of this section's height.
   `wrap` stacks them on a narrow viewport without compressing the strips. */
.band-rows {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem 2.5rem;
}

/* A column with the plot pushed to the foot, so the three lines share a baseline regardless of
   key height. The ages key has six bands and is always the tallest. */
.band-rows .band-row {
    flex: 1 1 24rem;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.band-plot {
    margin-top: auto;
}

/* Wider than the other two: its key has six bands with counts where theirs have one or two
   badges, and its bar is a proportion that uses the width, where theirs are fixed strips of
   thirty day columns. */
.band-rows .ages-row {
    flex: 2 1 32rem;
}

/* The key always sits on its own row under the name. Otherwise "out of service or unclear" (two
   badges) wraps to two rows and "not heard from" takes one, putting the two plots at different
   heights. */
.band-label {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.25rem;
    margin: 0 0 0.25rem;
}

.band-label .what {
    font-weight: 700;
}

.band-key {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    font-size: 0.85rem;
}

.band-plot {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 0.75rem;
    margin: 0;
}

svg.countline {
    display: block;
    /* The baseline is the axis. Without a rule under them the columns float, and a short column
       does not read as a small number. */
    border-bottom: 1px solid var(--rule-strong);
}

svg.countline .cell {
    stroke: var(--cell-edge);
    stroke-width: 0.5;
}

/* A pointer target on every day, including zero days. A zero day must not look like a day with
   no reading, which has its own line. */
svg.countline .hit {
    fill: transparent;
}

svg.countline .tick {
    fill: var(--ink);
}

/* A day cut short by the fetch limit. Drawn at the full column height behind the surviving
   count, because the bar is a partial count of that day. */
svg.countline .cut {
    fill: url(#p-truncated);
}

#p-truncated {
    color: var(--rule-strong);
}

/* The four source colours as fills, matching the left-border accents on the tiles and entry
   rows, so a source has one colour across the page. `--unknown`, `--ambiguous` and `--matched`
   are reused here: on /changes they name a subsystem, as the palette block at the top of this
   file notes. */
.c-status { fill: var(--source-status); }
.c-epiap  { fill: var(--unknown); }
.c-osm    { fill: var(--ambiguous); }
.c-match  { fill: var(--matched); }

/* The four source tiles share the row evenly: each holds a total, a name and a line. */
/* A column, so the graph can be pinned to the foot of the tile. `position: relative` is for the
   glyph: as a flex container the tile no longer floats the corner mark, and the mark belongs
   beside the total. */
.source-counts .tile {
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    position: relative;
}

.source-counts .tile .glyph {
    float: none;
    position: absolute;
    /* The tile's padding, so the mark sits in the corner. */
    top: 0.5rem;
    right: 0.75rem;
}

/* The line and its sentence at the foot of the box. The tiles are equal height, so `margin-top:
   auto` puts all four graphs on one baseline regardless of sentence length. */
.tile-line {
    margin-top: auto;
    padding-top: 0.6rem;
}

.line-peak {
    display: block;
    margin-bottom: 0.35rem;
    color: var(--muted);
    font-size: 0.85rem;
}

.line-peak .warn {
    color: var(--unknown);
    font-weight: 700;
}

.band-peak {
    color: var(--muted);
}

/* The coverage sparkline sits inside a tile, so it is smaller than the band and has no key: one
   series, named by the sentence above it. */
.coverage-line {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 0.5rem;
    margin: 0.25rem 0 0.5rem;
    font-size: 0.85rem;
    color: var(--muted);
}

/* ------------------------------------------------------------------ meters and age bands */

/* A proportion, drawn beside the number it represents. Every bar on this page sits next to its
   figure, so the bars are `role="presentation"` and none is the only way to read a value. */
svg.bar {
    display: block;
    width: 100%;
    height: 6px;
}

/* The track is drawn so a nearly empty bar reads as a small number and not as a missing bar. */
svg.bar .track {
    fill: var(--rule);
}

/* Against the track's mid grey, since --muted and --rule are too similar at this size to read a
   length against. */
svg.bar .fill {
    fill: var(--ink);
}

/* Over the limit uses the `unknown` texture as well as its hue, because exceeding the limit
   leads to that state: past the staleness TTL a lift becomes unknown/STALE. */
svg.bar.over .fill {
    fill: url(#p-unknown);
}

/* `margin-top: auto` in a flex column pins this to the foot of its container: the pane for the
   two live components, or the box itself for the export and OSM. The three ages are compared
   with each other, so they sit on one line. */
.meter-line {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: 0.25rem 0.5rem;
    margin: 1rem 0 0;
    margin-top: auto;
    padding-top: 0.75rem;
    font-size: 0.85rem;
    color: var(--muted);
}

.meter-line .limit {
    grid-column: 2;
    white-space: nowrap;
}

.meter-line .over {
    grid-column: 1 / -1;
    color: var(--unknown);
    font-weight: 700;
}

/* The only part-to-whole on this page: the bands partition the fleet, so they form one bar that
   fills its track. Wider than the two time-series lines because it has no day axis; its width is
   a proportion. */
svg.agesbar {
    display: block;
    width: 100%;
    min-width: 12rem;
    height: 12px;
}

/* Slices are separated by a surface-coloured gap. A border would thicken every slice, which with
   six slices in one bar would take up most of it. */
svg.agesbar .slice {
    stroke: var(--surface);
    stroke-width: 2;
}

/* Hue gives a band's tier; texture gives its position within the tier. All six are distinct,
   including the two amber ones: `expiring` is a lift about to be demoted, `expired` is one the
   sweep should have demoted already.

   All six patterns are horizontal-only. The daily 45-degree hatches and the dot cannot be
   reused: this bar is stretched to its column with independently scaled axes, so anything drawn
   across it washes out. See the <defs> in layout.html. */
.age-fresh    { fill: url(#p-age-fresh); }
.age-recent   { fill: url(#p-age-recent); }
.age-today    { fill: url(#p-age-today); }
.age-expiring { fill: url(#p-age-expiring); }
.age-expired  { fill: url(#p-age-expired); }
.age-never    { fill: url(#p-age-never); }

#p-age-fresh,
#p-age-recent,
#p-age-today { color: var(--available-mark); }
#p-age-fresh .ground,
#p-age-recent .ground,
#p-age-today .ground { fill: var(--available); }

#p-age-expiring,
#p-age-expired { color: var(--unknown-mark); }
#p-age-expiring .ground,
#p-age-expired .ground { fill: var(--unknown); }

#p-age-never { color: var(--no-data-mark); }
#p-age-never .ground { fill: var(--no-data-ground); }

/* The key is also the readout: every band, including zeros, with its count. That keeps the bar a
   single line; the numbers are here and not in slices too thin to hold a label. */
.ages-row .band-key {
    display: flex;
    flex-wrap: wrap;
    gap: 0.15rem 0.75rem;
    font-size: 0.85rem;
    font-weight: 400;
}

.ages-row .key-item {
    display: inline-flex;
    align-items: baseline;
    gap: 0.3rem;
    white-space: nowrap;
}

.ages-row .key-item .count {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.ages-row .key-item .label {
    color: var(--muted);
}

/* Twelve device pixels for a twelve-unit viewBox, so the pattern renders at the same scale as on
   the bar: a one-unit mark is one pixel in both, and the number of lines in the key matches the
   slice. */
svg.swatch {
    width: 12px;
    height: 12px;
    flex: none;
    align-self: center;
    border: 1px solid var(--rule-strong);
    border-radius: 2px;
}


.legend-items {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    list-style: none;
    padding: 0;
}

/* ------------------------------------------------------------------ the expanded detail */

.detail-body,
.audit-body {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    align-items: flex-start;
}

/* The audit trail is a second flex row under the detail row, and `gap` does not apply across
   containers; without this the Location and Transitions cards touch. */
.audit-body {
    margin-top: 1rem;
}

.block {
    border: 1px solid var(--rule-strong);
    border-radius: 4px;
    padding: 0.5rem 0.75rem;
    flex: 1 1 22rem;
    min-width: 0;
    background: var(--surface);
}

/* Each block's heading is on its own line; without the rule the heading ran into the list below
   and four blocks looked like one. */
.block > h4 {
    margin-top: 0;
    padding-bottom: 0.25rem;
    border-bottom: 1px solid var(--rule);
}

.block table {
    width: 100%;
    border-collapse: collapse;
}

.block th,
.block td {
    text-align: left;
    padding: 0.2rem 0.4rem;
    border-bottom: 1px solid var(--rule);
    vertical-align: top;
}

.map {
    height: 18rem;
    border: 1px solid var(--rule);
    background: var(--surface-alt);
}

.coordinates {
    font-size: 0.85rem;
    color: var(--muted);
}

pre {
    overflow-x: auto;
    background: var(--surface-alt);
    padding: 0.5rem;
    font-size: 0.85rem;
}

/* The sign is in the text; the colour repeats it. */
.diff-line {
    display: block;
    white-space: pre;
}

.d-added   { color: var(--added); background: var(--added-surface); }
.d-removed { color: var(--removed); background: var(--removed-surface); }
.d-meta    { color: var(--muted); font-weight: 700; }

.tag-changes {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* ------------------------------------------------------------------ the change log */

/* Source colours shared with the badges above. A source is identified by word and glyph; colour
   repeats it. */
.src-status { border-left-color: var(--source-status); }
.src-epiap  { border-left-color: var(--unknown); }
.src-osm    { border-left-color: var(--ambiguous); }
.src-match  { border-left-color: var(--matched); }

.badge.src-status { border-color: var(--source-status); color: var(--source-status); }
.badge.src-epiap  { border-color: var(--unknown); color: var(--unknown); }
.badge.src-osm    { border-color: var(--ambiguous); color: var(--ambiguous); }
.badge.src-match  { border-color: var(--matched); color: var(--matched); }

.filter-form {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem 1.25rem;
    align-items: flex-end;
    margin-bottom: 0.5rem;
}

/* The fields share the row, like the tiles below. `flex-basis: auto`: the basis is each
   control's own width, so the station select keeps the room its option text needs ("Amsterdam
   Centraal (8400058)" would truncate in a quarter of the row), and free space is shared equally
   on top. `min-width` lets a field shrink before the row wraps. */
.filter-form .field {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    margin: 0;
    flex: 1 1 auto;
    min-width: 8rem;
}

/* Without this the field grows but the control stays at its intrinsic width inside it. */
.filter-form .field select,
.filter-form .field input {
    width: 100%;
}

.filter-form label {
    color: var(--muted);
    font-size: 0.85rem;
}

/* The one field that does not stretch: a button a quarter of the row wide looks like a banner. */
.filter-form .actions {
    flex: 0 0 auto;
    flex-direction: row;
    align-items: baseline;
    gap: 0.75rem;
    min-width: 0;
}

.note {
    color: var(--muted);
    max-width: 60rem;
}

section.day {
    margin-bottom: 1rem;
}

section.day h3 {
    border-bottom: 1px solid var(--rule);
    padding-bottom: 0.2rem;
}

section.day .day-count {
    color: var(--muted);
    font-weight: 400;
}

ol.entries {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* One entry, one box. A left accent alone let entries of varying length run into each other.

   Compact, because most entries are one line: a typical day has ~930, and half a rem between
   each pair would add most of a screen of scrolling. The box separates adjacent one-line
   entries. */
/* A flex row, so the state change is pushed to the far right and every row's badges line up in a
   column. Inline flow started them wherever the station name ended, so "Hilversum" and "Amsterdam
   Sloterdijk" placed the same information differently on consecutive rows.

   Everything except the head and the transition takes a full line: the alert, the diffs, the tag
   lists. This uses `flex-basis: 100%`, so the markup and the selectors tests assert against stay
   unchanged. */
li.entry {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: 0.5rem;
    border: 1px solid var(--rule);
    border-left: 4px solid var(--rule-strong);
    border-radius: 4px;
    padding: 0.25rem 0.6rem;
    margin-bottom: 0.25rem;
}

/* `.document` also stays on the head's line: it is the feed log's equivalent of the change log's
   transition, a short link at the far right of every row. A 100% basis would put it on its own
   line under the head. */
li.entry > *:not(.head):not(.transition):not(.document) {
    flex-basis: 100%;
}

li.entry[data-source="status"] { border-left-color: var(--source-status); }
li.entry[data-source="epiap"]  { border-left-color: var(--unknown); }
li.entry[data-source="osm"]    { border-left-color: var(--ambiguous); }
li.entry[data-source="match"]  { border-left-color: var(--matched); }

/* From → to, on one baseline. The arrow is purely visual and hidden from screen readers: the badges
   on either side carry the word and glyph, and the "from"/"to" labels beside them are screen-reader
   text.

   `inline-flex`, here and on `.head`, puts the two paragraphs on one visual line: as inline-level
   boxes they flow side by side, and the whitespace between the tags in the template is the space
   between them. The common status entry (hundreds a day) is then one line: time, source, kind,
   station, lift, and transition. The times form a column because they are all HH:MM in tabular
   figures at a constant offset from the box edge.

   A flex `li.entry` with a 100% basis on every other child would make every alert, list and
   disclosure a flex item; they must stay ordinary blocks. Inline-level boxes shrink to fit and
   move whole to the next line when they do not fit, so only a long station name costs a second
   line. */
li.entry .transition {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    align-items: baseline;
    /* `auto` on the left pushes the state change to the far right, so every row's badges line up
       in a column. It is in this shorthand because a later `margin: 0` would reset a separate
       rule, which happened on the first attempt. */
    margin: 0 0 0 auto;
}

li.entry .transition .arrow {
    color: var(--muted);
    font-weight: 700;
}

/* The two phrases only the status source has: how many raw feed rows the debounce absorbed, and the
   date the feed gives for the change. Muted and small because they qualify the transition. Shown on
   the row, because the second one keeps an entry timed this morning from reading as a lift that
   broke this morning.

   They precede the transition: it is right-aligned, so a phrase after the badges would shift
   them left by its width and break the status column alignment. */
li.entry .absorbed,
li.entry .valid-from {
    color: var(--muted);
    font-size: 0.9rem;
}

li.entry .head {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: baseline;
    margin: 0;
}

li.entry .at {
    font-variant-numeric: tabular-nums;
    color: var(--muted);
}

/* The station is not muted: it links to the station's page, and a link styled as body text does
   not look clickable. It uses `var(--action)` and the site-wide hover/`:focus-visible` pair like
   the lift link beside it. */

/* The alert is a bold sentence on its own line, never only a colour or an icon. It is the reason
   the row exists. */
li.entry .alert {
    margin: 0.2rem 0 0;
    color: var(--not-available);
}

li.entry .alert .glyph {
    font-weight: 700;
}

li.entry .who,
li.entry .fields,
li.entry .tag-changes {
    margin: 0.2rem 0 0;
    font-size: 0.9rem;
}

li.entry .fields,
li.entry .tag-changes {
    list-style: none;
    padding: 0;
}

li.entry .fields .alerting code {
    font-weight: 700;
}

li.entry .tag-changes .word,
li.entry .fields .from {
    color: var(--muted);
}

/* The one remaining fold on an entry, styled at the weight of its content. Everything else is
   inherited from the site-wide `details:not(.station) > summary`, including marker and focus ring.

   A second fold over a status entry's provenance was removed: its summary already stated the
   count it revealed. The count is now a phrase on the row. */
li.entry details.diff > summary {
    font-size: 0.9rem;
}

li.entry details.diff .hint {
    color: var(--muted);
}

.legend-items .alert-text {
    color: var(--muted);
}

@media (prefers-reduced-motion: no-preference) {
    a,
    button,
    details.station > summary,
    details:not(.station) > summary {
        transition: background-color 120ms ease;
    }

    details.station > summary::before,
    button.detail-toggle .chevron {
        transition: transform 120ms ease;
    }
}

/* Paper is always light because the dark palette is scoped to `screen`. No rule here needs to
   restate a colour; if one does, the token it names should have been scoped. */
@media print {
    header.site nav,
    .skip {
        display: none;
    }

    details {
        /* A printed page cannot be expanded. */
        display: block;
    }
}

/* ------------------------------------------------------------------ the per-station page */

/* The permalink is in the disclosure body, where the table is edge-to-edge; without this it
   would touch the border the summary is padded away from. */
p.station-link {
    margin: 0.5rem 0.75rem;
}

.station-page > h2 .code {
    color: var(--muted);
    font-weight: 400;
    font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------------ the station's map

   `.map` further up gives the box its height and border; these rules style the section around it
   and the markers Leaflet draws inside it. */

section.station-map {
    margin: 1rem 0;
}

section.station-map > h3 {
    margin-bottom: 0.5rem;
}

/* The text equivalent under the map, the only version a screen reader gets. One lift per line,
   wrapping readably. */
section.station-map .coordinates {
    margin: 0.5rem 0 0;
    padding-left: 1.25rem;
}

/* A lift serving three levels emits three adjacent spans with no whitespace between them (one
   th:each), which read as `level 0 at 52.37890, 4.90030— level -1`. The gap is layout, so it is
   set here. */
section.station-map .coordinates span + span {
    margin-left: 0.35rem;
}

section.station-map .coordinates .code,
p.halteviewer .code {
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    color: var(--ink);
}

p.halteviewer {
    font-size: 0.9rem;
    color: var(--muted);
    max-width: 60rem;
}

/* A marker shows a status. It carries the same glyph as the badge and legend, so broken and
   working lifts are distinguishable in greyscale and with deuteranopia, for which #b3261e and
   #1b5e20 are 1.21:1 apart. Leaflet builds these at run time; they are not in the served markup. */
.lift-marker {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border: 2px solid currentColor;
    border-radius: 50%;
    background: var(--surface);
    font-weight: 700;
    line-height: 1;
}

.lift-marker.st-notAvailable { color: var(--not-available); }
.lift-marker.st-unknown      { color: var(--unknown); }
.lift-marker.st-available    { color: var(--available); }
.lift-marker.st-noData       { color: var(--no-data); }

/* The same status on a lift mapped as an area. `stroke` on every shape, `fill` only on closed
   ones; see pathStyle() in dashboard.js. The dash pattern is the texture alongside the hue, as
   day cells have a pattern. */
.lift-shape.st-notAvailable { stroke: var(--not-available); }
.lift-shape.st-unknown      { stroke: var(--unknown); }
.lift-shape.st-available    { stroke: var(--available); }
.lift-shape.st-noData       { stroke: var(--no-data); }

.lift-area.st-notAvailable { fill: var(--not-available); }
.lift-area.st-unknown      { fill: var(--unknown); }
.lift-area.st-available    { fill: var(--available); }
.lift-area.st-noData       { fill: var(--no-data); }

/* Leaflet's controls in the dark scheme. The vendored stylesheet has forty-one light literals and
   is not edited, so upgrading stays a re-download of the directory. These overrides work because
   our stylesheet loads second. Each matches the specificity of the rule it overrides, so if the
   vendored file changes they stop applying entirely.

   The tiles are not changed. The usual `filter: invert(1) hue-rotate(180deg)` would also invert
   the place-name labels baked into OSM raster tiles, leaving unreadable grey-on-charcoal text.
   Only the surrounding controls (popup, zoom buttons, attribution) are darkened, so the map
   reads as a bright plate in a dark frame. */
@media screen and (prefers-color-scheme: dark) {
    /* The background behind the tiles, visible while they load and beyond the edge of the world.
       Leaflet's #ddd flashes bright on every pan. */
    .leaflet-container {
        background: var(--surface-alt);
    }

    .leaflet-popup-content-wrapper,
    .leaflet-popup-tip {
        color: var(--ink);
        background: var(--surface);
    }

    .leaflet-container a.leaflet-popup-close-button {
        color: var(--muted);
    }

    .leaflet-container a.leaflet-popup-close-button:hover,
    .leaflet-container a.leaflet-popup-close-button:focus-visible {
        color: var(--ink);
    }

    .leaflet-bar a {
        color: var(--ink);
        background-color: var(--surface);
        border-bottom-color: var(--rule);
    }

    /* `:focus-visible`, the twin used throughout this file, so the zoom buttons show keyboard
       focus like every other control. */
    .leaflet-bar a:hover,
    .leaflet-bar a:focus-visible {
        background-color: var(--surface-alt);
    }

    .leaflet-bar a.leaflet-disabled {
        color: var(--rule);
        background-color: var(--surface-alt);
    }

    /* Translucent, as in the vendored rule: the attribution covers a map corner, and an opaque
       bar would hide the tile. Its links need no rule; `a { color: var(--action) }` at the top
       of this file already applies. */
    .leaflet-container .leaflet-control-attribution {
        color: var(--muted);
        background: color-mix(in srgb, var(--surface) 80%, transparent);
    }
}

/* ------------------------------------------------------------------ the hourly line

   `/elevators/{ref}` only.

   One lift's page draws about 720 cells per line; `/` and `/stations/{code}` draw up to 446 lifts
   and stay daily, because 24 cells a day there would be some 321,000 rectangles in one document.
   The fold puts the `scale-hour` class on the row to scope this. */

/* Full width scales the SVG; the cells do not get wider. The viewBox sets the coordinate system,
   so the line is stretched to the content column at a fixed height. `preserveAspectRatio="none"`
   in the markup lets the axes scale independently; uniform scaling would derive the height from
   the width, giving a three-pixel-tall line on a narrow viewport. */
svg.timeline.scale-hour {
    width: 100%;
}

svg.timeline.scale-hour.line-status {
    height: 22px;
}

svg.timeline.scale-hour.line-pairing {
    height: 13px;
}

/* No hairline around an hour. The daily cell's 0.5-unit stroke is a quarter of the cell width
   here and scales into a grey wash over the whole line. No gap either, since a gap between
   two-pixel cells would be most of the line. Cells are separated by colour and texture, and days
   by the rule below. `crispEdges` because the cells abut exactly: antialiasing 720 shared edges
   leaves a seam on each, even between cells of the same colour. */
svg.timeline.scale-hour .cell {
    stroke: none;
    shape-rendering: crispEdges;
}

/* Midnight. `non-scaling-stroke` keeps the line one device pixel wide at any x scale; a 1-unit
   rect would be a third of a pixel at full width and disappear into the neighbouring cells. */
svg.timeline .dayline {
    stroke: var(--surface);
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
}

/* A texture designed for a 4px day cell is invisible in a 2px hourly one: the x axis is scaled
   to about a third, so anything the pattern draws across the cell (the cross-hatch, the dot)
   falls below a pixel and blurs into flat colour. The y axis is not scaled down, so these
   patterns use horizontal bands, thick or thin, light or dark. `available` and `matched` stay
   solid at both resolutions, so a textured cell still reads as "something happened here". */
svg.timeline.scale-hour .c-notAvailable { fill: url(#p-notAvailable-hour); }
svg.timeline.scale-hour .c-unknown      { fill: url(#p-unknown-hour); }
svg.timeline.scale-hour .c-noData       { fill: url(#p-noData-hour); }
svg.timeline.scale-hour .c-ambiguous    { fill: url(#p-ambiguous-hour); }
svg.timeline.scale-hour .c-suspect      { fill: url(#p-suspect-hour); }
svg.timeline.scale-hour .c-unmatched    { fill: url(#p-unmatched-hour); }

/* A text description under the line: 720 cells without a scale do not show how much time they
   cover. */
p.timeline-note {
    margin: 0.25rem 0 1rem;
    font-size: 0.85rem;
    color: var(--muted);
}

/* ------------------------------------------------------------------ the feed log */

/* The four kinds, using the change log's source colour vocabulary. `conditions` uses
   --source-status because conditions are the status source's input. The two failure kinds use
   the two warning colours. Heartbeats use the muted colour because they are the ordinary case,
   and colouring them would make every quiet hour look eventful. */
.badge.fk-conditions { border-color: var(--source-status); color: var(--source-status); }
.badge.fk-heartbeat  { border-color: var(--no-data); color: var(--no-data); }
.badge.fk-failure    { border-color: var(--not-available); color: var(--not-available); }
.badge.fk-malformed  { border-color: var(--unknown); color: var(--unknown); }

.feed-log li.entry .technical > span + span::before {
    /* A separator in the stylesheet, so a screen reader does not read a bullet between "709 B"
       and "/DOVA". */
    content: ", ";
}

/* The document link, at the far right of every row so the links form a column. This requires the
   link to be a sibling of `.head`: `auto` needs free space on the row, and an inline-flex box
   shrinks to its content and leaves none. The `auto` is in the shorthand for the reason given at
   `li.entry .transition`: a later `margin: 0` reset the standalone rule. */
.feed-log li.entry .document {
    margin: 0 0 0 auto;
    white-space: nowrap;
}

/* The count that replaces the list beyond `FeedEntry.NAMED_LIFTS`. Styled like the lift links it
   replaces, since it links to the message that names them. */
.feed-log li.entry .lift-count {
    white-space: nowrap;
}

/* One message can name most of the fleet. The list is bounded and scrolls, so the document below is
   not pushed ninety lines down. The count is in the `dt`, which tells a reader whether the box has
   more.

   `min-width: 0` is needed: a grid item's automatic minimum is its content, and each ref is one
   unbreakable token, so without it the `1fr` column grows to the longest run and the card
   overflows the page sideways. */
.feed-message-page .lifts {
    display: flex;
    flex-wrap: wrap;
    gap: 0.15rem 0.75rem;
    min-width: 0;
    max-height: 11rem;
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* A real list, separate from `.lifts` above: each entry is a ref, a status and a sentence, and a
   sentence needs its own line. No height cap; there are few, and they are what the reader is
   looking for. */
.feed-message-page .conditions {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    min-width: 0;
}

/* Muted and monospaced: a wire value quoted beside a sentence, and the sentence is the focus. */
.feed-message-page .conditions .status {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.85em;
    color: var(--muted);
}

/* The gateway's verdict, so it is styled apart from both the wire value above and the Dutch
   sentence beside it. Uses `--not-available` because it says a record was set aside, which is
   what a reader opening this page to find why a status did not change is looking for. */
.feed-message-page .conditions .outcome {
    display: block;
    font-size: 0.85em;
    font-style: italic;
    color: var(--not-available);
}

/* The document takes the whole row, wider than the 22rem `.block` basis of its neighbour. In a
   22rem column, 700 bytes of XML wraps unreadably, as the inline version on the lift page did. */
.feed-message-page .message-body {
    flex-basis: 100%;
}

/* The toggle between the two views, and the note saying which is shown. One line, so the label
   and caveat are read together. */
.feed-message-page .view-toggle {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.5rem 0.75rem;
}

.feed-message-page .message-body pre {
    /* The document gets the full width and wraps, so long lines are readable on a printout. */
    white-space: pre-wrap;
    word-break: break-word;
    max-height: none;
}

/* A message that changed nothing. Muted text, because an empty status cell would look like a
   rendering fault. */
.transitions .no-change {
    color: var(--muted, var(--no-data));
    font-style: italic;
}

/* The link to the document, on its own line under its badge. The document is 700 bytes and this
   is the narrowest of four columns, so it is not inlined. */
.transitions .message-link {
    display: block;
    margin-top: 0.35rem;
}

.transitions .feed-note {
    color: var(--no-data);
    font-size: 0.9em;
}

/* ------------------------------------------------------------------ narrow viewports

   A table is as wide as its columns: the lift tables' five need some 45rem and the audit trail's
   six about as much, more than any phone has. Each scrolls sideways in its own box so the page does
   not; a sideways-scrolling page moves the header and the reader's place with it. Every such table
   contains links, so the box needs no tabindex to be scrolled from the keyboard. */
/* The four fixed columns are 43rem; the description, the only flexible one, keeps at least 9rem.
   Without a minimum, a fixed-layout table squeezes that column to one letter per line on a phone
   and does not overflow into the scroll box. */
.table-scroll > table.lifts {
    min-width: 52rem;
}

.table-scroll {
    overflow-x: auto;
    /* The containing block for everything absolutely positioned inside, notably the `.sr-only`
       text: an absolute box escapes the clip of a non-positioned overflow ancestor, and every
       row's hidden "Detail for lift …" then widened the page. */
    position: relative;
}

/* Below about 40rem, side padding and tile minimum widths push the page past the viewport: three
   22rem fact tiles cannot become one column if each requires 22rem, and 1.5rem of padding per
   side is a tenth of the screen. The tiles keep their content width as a basis but may shrink
   below it. */
@media (max-width: 40rem) {
    header.site,
    footer.site,
    main {
        padding-left: 0.75rem;
        padding-right: 0.75rem;
    }

    .tile,
    .facts .tile {
        min-width: 0;
        flex-basis: 100%;
    }

    /* The value column may shrink below its content, and content may break to fit: a facility
       ref or a timestamp with its age is one unbreakable token wider than the space beside its
       term. */
    dl {
        grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
    }

    dd {
        overflow-wrap: anywhere;
    }

    /* Pills stay on one line where there is room. Here there may not be: the legend's pills hold
       whole sentences, and a status with its reason can be wider than its column. */
    .badge {
        white-space: normal;
        flex-wrap: wrap;
    }
}
