/* ═══════════════════════════════════════════════════════════════
   Reset

   Bootstrap's Reboot used to provide this. Removing Bootstrap removed it too,
   and the border-box rule in particular is load-bearing: without it .content's
   16px padding was ADDED to its 100% width and every page overflowed its own
   viewport by 32px. Stated explicitly here so the app owns its own baseline
   instead of inheriting one from a dependency it no longer has.
   ═══════════════════════════════════════════════════════════════ */

*,
*::before,
*::after {
    box-sizing: border-box;
}

body {
    margin: 0;
}

/* Headings and paragraphs arrive with UA margins that fight the spacing scale.
   Components set their own; anything that wants the default asks for it. */
h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd {
    margin: 0;
}

/* Media should never be the thing that widens a layout.

   `canvas` is deliberately NOT in this list. Leaflet's panes are zero-sized
   positioning containers, so `max-width: 100%` on a canvas inside one resolves
   against a width of 0 and collapses it — which silently killed the map's flow
   overlay while leaving its width ATTRIBUTE correct, so it looked fine in the
   DOM and drew nothing. Every canvas in this app is sized explicitly by the
   code that owns it; none of them needs this guard. */
img, svg, video {
    max-width: 100%;
    height: auto;
}

/* Form controls do not inherit the page font by default. */
button, input, select, textarea {
    font: inherit;
    color: inherit;
}

html, body {
    font-family: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
    font-size: var(--pt-text-base, 16px);
    -webkit-font-smoothing: antialiased;
    background: var(--pt-bg-page);
    color: var(--pt-fg-primary);
}

/* Headings shouldn't end on a lonely word; body copy shouldn't end on a lonely
   line. Both are one declaration each and degrade to nothing where unsupported. */
h1, h2, h3, h4, .pt-balance { text-wrap: balance; }
p, .pt-pretty { text-wrap: pretty; }

/* Trim the font's built-in leading above the cap height and below the baseline,
   so a heading's visual gap to what follows equals the margin you actually
   wrote. Without it every heading carries a few invisible pixels of half-leading
   and the 8px spacing grid never quite lands. Unsupported browsers ignore it. */
@supports (text-box: trim-both cap alphabetic) {
    h1, h2, h3, h4, .pt-page-title, .pt-departure-hero, .pt-answer-headline {
        text-box: trim-both cap alphabetic;
    }
}

/* Numbers that change in place — countdowns, durations, costs — must not
   reflow their neighbours as digits swap width. */
.pt-tabular, time, output { font-variant-numeric: tabular-nums; }

/* Inputs that grow with their content instead of scrolling a fixed box.
   Scoped to the app's own controls: Radzen sizes its own inputs. */
@supports (field-sizing: content) {
    textarea:not(.rz-textarea) { field-sizing: content; }
}

h1:focus {
    outline: none;
}

a {
    color: var(--pt-color-brand-on-surface);
}

/* One focus ring for everything, drawn with outline so it follows the shape and
   never gets clipped by an ancestor's overflow. The old rule painted a white
   inner ring, which vanished against a dark surface. */
:focus-visible {
    outline: 2px solid var(--pt-color-brand-on-surface);
    outline-offset: 2px;
    border-radius: var(--pt-radius-xs);
}

.content {
    /* The bottom nav is fixed and only exists on narrow screens; its height and
       the space reserved for it here both come from --pt-bottom-nav-safe, so
       they cannot drift apart again. */
    padding: var(--pt-space-5) var(--pt-space-4)
             calc(var(--pt-space-8) + var(--pt-bottom-nav-safe));
    width: 100%;
    margin-inline: auto;

    /* Every page's root query container. Components inside reflow against the
       content column — which is capped at 1180px and centred — rather than
       against the window, so a page rendered in a narrower shell later still
       lays out correctly. */
    container-type: inline-size;
    container-name: pt-content;
}

@media (min-width: 640px) {
    .content {
        /* No bottom nav past the phone breakpoint — reclaim the reserve. */
        padding-bottom: var(--pt-space-8);
    }
}

@media (min-width: 1024px) {
    .content {
        /* Long measure hurts readability; cap it and centre. */
        max-width: 1180px;
        margin-inline: auto;
        padding-inline: var(--pt-space-6);
    }
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid var(--pt-color-success);
}

.invalid {
    outline: 1px solid var(--pt-color-danger);
}

.validation-message {
    color: var(--pt-color-danger);
}

#blazor-error-ui {
    color-scheme: light only;
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(var(--pt-shadow-rgb), 0.2);
    box-sizing: border-box;
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

    #blazor-error-ui .dismiss {
        cursor: pointer;
        position: absolute;
        right: 0.75rem;
        top: 0.5rem;
    }

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.loading-progress {
    position: absolute;
    display: block;
    width: 8rem;
    height: 8rem;
    inset: 20vh 0 auto 0;
    margin: 0 auto 0 auto;
}

    .loading-progress circle {
        fill: none;
        stroke: var(--pt-border-subtle);
        stroke-width: 0.6rem;
        transform-origin: 50% 50%;
        transform: rotate(-90deg);
    }

        .loading-progress circle:last-child {
            stroke: var(--pt-color-brand-600);
            stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
            transition: stroke-dasharray 0.05s ease-in-out;
        }

.loading-progress-text {
    position: absolute;
    text-align: center;
    font-weight: bold;
    inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}

    .loading-progress-text:after {
        content: var(--blazor-load-percentage-text, "Loading");
    }

code {
    color: var(--pt-color-danger);
}

/* ═══════════════════════════════════════════════════════════════
   Small shared pieces. Keyframes are NOT redeclared here — they live
   in pt-motion.css, which is the single home for them.

   A block of `--pt-brand-*` / `--pt-accent-*` aliases used to sit here for
   "any third-party component that still references them". Nothing did, in this
   project or in Radzen, so they were a second vocabulary for colours that
   already have names.
═══════════════════════════════════════════════════════════════ */
/* The disclosure chevron. Both the `.open` class (PollHistoryTable's own bool)
   and the native `[open]` on <details> (route detail) drive the same rotation,
   and both live here rather than in a scoped sheet: the chevron is a <PtIcon>,
   so a scoped rule would compile its last compound to `.pt-collapsible-icon[b-…]`
   and never match the child component's span. */
.pt-collapsible-icon { display: inline-block; transition: transform var(--pt-dur-base) var(--pt-ease); }
.pt-collapsible-icon.open,
details[open] > summary .pt-collapsible-icon { transform: rotate(90deg); }

/* ═══════════════════════════════════════════════════════════════
   Icons. One system: Material Symbols, loaded once in index.html and used
   through <PtIcon>. Emoji were the third icon system in the app (after this
   font and Radzen's own) and the only one that ignores `color`, ships a
   different drawing per operating system, and cannot be sized off the type
   scale. `font-size` is the optical-size axis here, so the two stay in step.
═══════════════════════════════════════════════════════════════ */
.pt-icon {
    font-family: 'Material Symbols Outlined';
    font-weight: normal;
    font-style: normal;
    line-height: 1;
    letter-spacing: normal;
    text-transform: none;
    display: inline-block;
    white-space: nowrap;
    word-wrap: normal;
    direction: ltr;
    -webkit-font-feature-settings: 'liga';
    -webkit-font-smoothing: antialiased;
    font-variation-settings: 'opsz' 24, 'wght' 400, 'GRAD' 0, 'FILL' var(--pt-icon-fill, 0);
    font-size: var(--pt-icon-size, 1.25em);
    flex-shrink: 0;
    /* The glyph box is taller than the mark; without this an icon next to text
       pushes the line box and misaligns every label it sits beside. */
    vertical-align: -0.15em;
}

.pt-icon-filled { --pt-icon-fill: 1; }
.pt-icon-sm { --pt-icon-size: 1em; }
.pt-icon-lg { --pt-icon-size: 1.75em; }
.pt-icon-xl { --pt-icon-size: 2.5em; }

/* Per-context icon tints.

   These have to be global. <PtIcon> is a component, so its span never receives
   the `b-…` scope attribute of the page that placed it, and Blazor scopes a
   rule by rewriting its LAST compound selector — `.auth-feature .pt-icon` in a
   scoped sheet becomes `.auth-feature .pt-icon[b-…]` and matches nothing. Any
   rule whose final selector names an element another component renders belongs
   here, not in a .razor.css. */

/* Sign-in panel feature list: icons take the panel's accent so the list reads
   as four marked items rather than four oddly-starting sentences. */
.auth-feature .pt-icon {
    color: var(--pt-color-brand-300);
    --pt-icon-size: 1.5rem;
}

/* User-menu items: the icon is subordinate to its label, except on the
   destructive item where it should carry the same red as the words. */
.pt-dropdown-item .pt-icon { color: var(--pt-fg-tertiary); }
.pt-dropdown-item.danger .pt-icon { color: currentColor; }

/* ═══════════════════════════════════════════════════════════════
   Spacing utilities.

   These exist because Radzen's rz-mb-4 / rz-mt-3 / rz-p-6 are FIXED rem
   values. The app's density control works by scaling --pt-density, which every
   --pt-space-* step multiplies through — so a page laid out with rz-* utilities
   kept desktop spacing at "Compact" while the components beside it tightened.
   Half the app responded to the setting and half ignored it, which read as
   inconsistent alignment rather than as a density choice.

   Same names, same scale positions, density-aware values.
═══════════════════════════════════════════════════════════════ */
.pt-mt-1 { margin-top: var(--pt-space-1); }
.pt-mt-2 { margin-top: var(--pt-space-2); }
.pt-mt-3 { margin-top: var(--pt-space-3); }
.pt-mt-4 { margin-top: var(--pt-space-4); }
.pt-mt-6 { margin-top: var(--pt-space-6); }

.pt-mb-0 { margin-bottom: 0; }
.pt-mb-1 { margin-bottom: var(--pt-space-1); }
.pt-mb-2 { margin-bottom: var(--pt-space-2); }
.pt-mb-3 { margin-bottom: var(--pt-space-3); }
.pt-mb-4 { margin-bottom: var(--pt-space-4); }
.pt-mb-6 { margin-bottom: var(--pt-space-6); }

.pt-p-2 { padding: var(--pt-space-2); }
.pt-p-3 { padding: var(--pt-space-3); }
.pt-p-4 { padding: var(--pt-space-4); }
.pt-p-5 { padding: var(--pt-space-5); }
.pt-p-6 { padding: var(--pt-space-6); }

.pt-w-100 { width: 100%; }

.pt-flex { display: flex; }
.pt-items-center { align-items: center; }
.pt-wrap { flex-wrap: wrap; }
.pt-gap-2 { gap: var(--pt-space-2); }
.pt-gap-3 { gap: var(--pt-space-3); }
.pt-gap-4 { gap: var(--pt-space-4); }
.pt-mr-3 { margin-right: var(--pt-space-3); }

/* Vertical rhythm for a stack of sections — one rule instead of a margin class
   on every child, and it cannot leave a trailing margin at the end of a card. */
.pt-stack > * + * { margin-top: var(--pt-space-4); }
.pt-stack-lg > * + * { margin-top: var(--pt-space-6); }

/* ═══════════════════════════════════════════════════════════════
   Navigation links — top bar and bottom bar.

   These are global rather than scoped, and they have to be. Both bars render
   their links with <NavLink>, which is a COMPONENT: Blazor's CSS isolation
   stamps its `b-…` scope attribute on the plain HTML written in a .razor file,
   never on the markup a child component renders. So a `.app-nav-link` rule in
   NavMenu.razor.css compiled to `.app-nav-link[b-mhqeozcjir]` and matched
   nothing — the top nav had been falling through to the global `a` rule, drawn
   as underlined brand-blue with no padding and no active state, for as long as
   NavLink has been used there.

   The colours are pinned to the fixed end of the neutral ramp because the shell
   is a dark bar in BOTH themes. The inactive step used to be neutral-200 at 70%
   alpha, which composites to roughly 4.2:1 on that bar — under AA for text this
   size, on the app's primary navigation. Full-strength neutral-300 clears it at
   about 7:1, and the active state now separates itself by fill and weight
   rather than by being the only legible one.
═══════════════════════════════════════════════════════════════ */
.app-nav-link {
    display: inline-flex;
    align-items: center;
    gap: var(--pt-space-2);
    color: var(--pt-color-neutral-300);
    padding: var(--pt-space-2) var(--pt-space-3);
    border-radius: var(--pt-radius-sm);
    text-decoration: none;
    font-size: var(--pt-text-sm);
    font-weight: var(--pt-weight-medium);
    letter-spacing: 0.01em;
    box-shadow: inset 0 0 0 1px transparent;
    transition: background var(--pt-dur-fast) var(--pt-ease),
                color var(--pt-dur-fast) var(--pt-ease),
                box-shadow var(--pt-dur-fast) var(--pt-ease);
    white-space: nowrap;
}

.app-nav-link:hover {
    background: color-mix(in oklab, var(--pt-color-neutral-0) 8%, transparent);
    color: var(--pt-color-neutral-0);
}

.app-nav-link.active {
    background: color-mix(in oklab, var(--pt-color-brand-500) 20%, transparent);
    box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--pt-color-brand-400) 40%, transparent);
    color: var(--pt-color-neutral-0);
    font-weight: var(--pt-weight-semibold);
}

.app-nav-link:focus-visible {
    outline: 2px solid var(--pt-color-brand-300);
    outline-offset: 2px;
}

/* Bottom bar — same NavLink problem, same solution. */
.pt-bottom-nav-link {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--pt-space-1);
    color: var(--pt-fg-tertiary);
    text-decoration: none;
    font-size: var(--pt-text-xs);
    font-weight: var(--pt-weight-medium);
    min-width: 4rem;
    min-height: 3rem;
    border-radius: var(--pt-radius-md);
    transition: color var(--pt-dur-fast) var(--pt-ease);
    -webkit-tap-highlight-color: transparent;
}

.pt-bottom-nav-link:active { color: var(--pt-color-brand-on-surface); }

/* The bar had no active state at all, so on a phone — where it IS the
   navigation — nothing on screen said which page you were on. The icon fills as
   well as changing colour, because colour alone is not a state indicator. */
.pt-bottom-nav-link.active {
    color: var(--pt-color-brand-on-surface);
    font-weight: var(--pt-weight-semibold);
}

.pt-bottom-nav-link.active .pt-bottom-nav-icon { --pt-icon-fill: 1; }

.pt-bottom-nav-icon { --pt-icon-size: 1.5rem; }

/* The FAB is a brand-filled disc at every state — it must not pick up the
   active or pressed colour its siblings use. */
.pt-bottom-nav-fab,
.pt-bottom-nav-fab:active,
.pt-bottom-nav-fab.active {
    color: var(--pt-fg-on-brand);
}

/* ═══════════════════════════════════════════════════════════════
   Radzen select bar.

   Three corrections, all of them Radzen defaults that read wrong here:

   1. The bar ships `display: inline-flex`, so it sat on the same line as the
      caption <span> above it — "System follows your device setting…" and the
      three buttons shared a row. Block-level, and it starts its own line.
   2. Each item is a <button> containing <i class="rz-button-icon-left"> and
      <span class="rz-button-text"> with no margin between them, so the icon was
      welded to the label ("computerSystem"). The button is a flex row with a
      gap instead — which also centres the glyph against the text rather than
      sitting it on the baseline.
   3. The selected item's colour comes from --rz-selectbar-selected-color, now
      mapped in pt-tokens.css; the fill is pinned here to the same brand step
      the primary button uses so the pair is a guaranteed contrast ratio rather
      than whatever --rz-primary resolves to per theme (brand-400 on dark would
      have put white text at about 2.5:1).
═══════════════════════════════════════════════════════════════ */
.rz-selectbar {
    display: flex;
    flex-wrap: wrap;
}

.rz-selectbar .rz-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--pt-space-2);
}

.rz-selectbar .rz-button.rz-state-active {
    background: var(--pt-color-brand-600);
    color: var(--pt-fg-on-brand);
}

.rz-selectbar .rz-button.rz-state-active .rzi {
    color: inherit;
}

/* ═══════════════════════════════════════════════
   C2: Compact Dashboard Status Bar
═══════════════════════════════════════════════ */
.pt-status-bar {
    display: flex;
    align-items: center;
    gap: var(--pt-space-5);
    padding: var(--pt-space-2) var(--pt-space-4);
    background: var(--pt-bg-surface);
    border-radius: var(--pt-radius-md);
    border: 1px solid var(--pt-border-subtle);
    box-shadow: var(--pt-elev-1);
    margin-bottom: var(--pt-space-6);
    flex-wrap: wrap;
}

.pt-status-dot {
    width: 0.5rem;
    height: 0.5rem;
    border-radius: var(--pt-radius-full);
    display: inline-block;
    margin-right: var(--pt-space-1);
    flex-shrink: 0;
}

.pt-status-dot-green { background: var(--pt-color-success); }
.pt-status-dot-amber { background: var(--pt-color-warning); }
.pt-status-dot-red   { background: var(--pt-color-danger); }

/* Connectivity is a property of the whole bar, not of the one chip that
   mentions it. :has() lets the container react to its own contents, so losing
   the connection tints the entire strip instead of hiding the news in a corner
   the eye has no reason to visit. */
.pt-status-bar:has(.pt-status-offline) {
    border-color: color-mix(in oklab, var(--pt-color-warning) 55%, var(--pt-border-subtle));
    background: color-mix(in oklab, var(--pt-color-warning) 8%, var(--pt-bg-surface));
}

.pt-status-item {
    font-size: var(--pt-text-sm);
    font-weight: var(--pt-weight-medium);
    color: var(--pt-fg-secondary);
    display: flex;
    align-items: center;
    gap: var(--pt-space-1);
    white-space: nowrap;
}

/* The bar's trailing-action slot. The dashboard's Add Route button used to live
   here; it is in the page header now, where a primary action belongs and where
   it does not disappear along with the bar whenever connectivity is fine. */
.pt-status-bar-actions {
    margin-left: auto;
}

/* ═══════════════════════════════════════════════
   UI-1/2/3: Route Card (C1 Dashboard)
═══════════════════════════════════════════════ */
/* The card list. On a desktop the content column is capped at 1180px and every
   card is a single short row, so a one-per-line stack left roughly half the
   viewport empty and pushed the fourth commute below the fold for no reason.
   auto-fit + minmax means the column count is decided by available width rather
   than by a breakpoint: one column on a phone, two past ~46rem, and no media
   query anywhere. */
.pt-route-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
    /* Stretch, not start. A route that has samples carries a sparkline and one
       that doesn't carries a line of text, so side-by-side cards used to differ
       in height by the whole height of the sparkline — two cards showing the
       same kind of thing, drawn as two different shapes. Every card in a row is
       the tallest card's height now; the extra space falls below the content. */
    align-items: stretch;
    gap: var(--pt-space-4);
}

/* Each cell is its own query container, so a card is laid out by the width of
   the COLUMN it landed in — not the window and not the list. That is what makes
   the same component correct at one-up on a phone, at two-up on a desktop, and
   in any narrower slot it gets dropped into later. Declared on the wrapper
   rather than on .pt-route-cards because a container cannot query itself. */
.pt-route-cards > * {
    container-type: inline-size;
    container-name: pt-cards;
    min-width: 0;
}

/* Carry the stretched row height down through the two wrappers between the grid
   cell and the card itself, or the card keeps its content height and the cell
   just grows an empty gap under it. */
.pt-route-cards > *,
.pt-route-cards .pt-swipe-wrap,
.pt-route-cards .pt-route-card {
    height: 100%;
}

.pt-route-card {
    background: var(--pt-bg-surface);
    border-radius: var(--pt-radius-lg);
    border: 1px solid var(--pt-border-subtle);
    border-left: 5px solid var(--pt-ambient-color, var(--pt-color-brand-500));
    box-shadow: var(--pt-elev-2);
    transition: box-shadow var(--pt-dur-fast) var(--pt-ease),
                transform var(--pt-dur-fast) var(--pt-ease);
    overflow: hidden;
}

/* Lift only where there is a cursor to lift toward. */
@media (hover: hover) {
    .pt-route-card:hover {
        box-shadow: var(--pt-elev-3);
        transform: translateY(-1px);
    }
}

/* The sample route wears its border the way its flag reads: dashed and
   provisional, not one of your real commutes. */
.pt-route-card:has(.pt-route-flag-demo) {
    border-style: dashed;
    border-left-style: solid;
}

/* The card navigates, so it behaves like the control it is. */
.pt-route-card {
    cursor: pointer;

    /* Lifts the card above the absolutely-positioned swipe actions behind it.
       Without a position of its own the card stays in the static painting layer,
       which loses to any positioned sibling regardless of DOM order — so the
       "Check Now" and "Delete" panels rendered ON TOP of the card and covered
       the departure time. They are meant to be revealed by the card sliding
       away, not painted over it. */
    position: relative;
    z-index: 1;
    background: var(--pt-bg-surface);
}
.pt-route-card:focus-visible {
    outline: 2px solid var(--pt-color-brand-on-surface);
    outline-offset: 2px;
}

.pt-route-card-body {
    padding: var(--pt-space-3) var(--pt-space-5);
    display: flex;
    align-items: center;
    gap: var(--pt-space-4);
}

/* The answer takes the row and the sparkline takes what's left. The sparkline's
   own wrapper is width:100%, so without a basis here it claimed the whole row
   and wrapped "Leave by 6:00 AM" onto four lines. */
.pt-route-card-answer {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--pt-space-1);
}

.pt-route-card-body > .pt-sparkline-wrap {
    flex: 0 1 40%;
    max-width: 22rem;
    margin-top: 0;
}

/* Narrow column: the sparkline drops below the answer instead of squeezing it. */
@container pt-cards (width < 26rem) {
    .pt-route-card-body {
        flex-direction: column;
        align-items: stretch;
        gap: var(--pt-space-2);
        padding: var(--pt-space-3) var(--pt-space-4);
    }

    .pt-route-card-foot { padding-inline: var(--pt-space-4); }
    .pt-departure-hero { font-size: var(--pt-text-lg); }
}

/* The route line — the street pair, biggest on the card. */
.pt-route-card-route {
    display: flex;
    align-items: center;
    gap: var(--pt-space-2);
    color: var(--pt-fg-primary);
    font-weight: var(--pt-weight-semibold);
    font-size: var(--pt-text-base);
    line-height: var(--pt-leading-tight);
    min-width: 0;
}

.pt-route-card-time {
    font-size: var(--pt-text-sm);
    color: var(--pt-fg-secondary);
    font-variant-numeric: tabular-nums;
}

.pt-route-card-time-value {
    font-weight: var(--pt-weight-medium);
    color: var(--pt-fg-secondary);
}

.pt-route-card-time-pending {
    color: var(--pt-fg-tertiary);
}

/* Address line inside the card body. Each side can ellipsis-truncate if the
   street name is long, but the arrow stays visible. */
.pt-route-addr-from,
.pt-route-addr-to {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

.pt-route-addr-arrow {
    color: var(--pt-fg-faint);
    flex-shrink: 0;
}

/* Two states, and only two, are allowed to interrupt the answer. */
.pt-route-flag {
    /* Floats to the top-right corner of the card so the route line and travel
       time stay left-aligned without competing for horizontal space. */
    position: absolute;
    top: var(--pt-space-3);
    right: var(--pt-space-3);
    flex-shrink: 0;
    font-size: var(--pt-text-xs);
    font-weight: var(--pt-weight-bold);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: var(--pt-space-1) var(--pt-space-2);
    border-radius: var(--pt-radius-full);
    line-height: 1;
    background: var(--pt-bg-surface);
}

.pt-route-flag-demo {
    color: var(--pt-color-info);
    border: 1px dashed var(--pt-color-info);
}

.pt-route-flag-paused {
    color: var(--pt-color-warning);
    border: 1px solid color-mix(in oklab, var(--pt-color-warning) 45%, transparent);
}

/* Ambient traffic state colors */
.pt-ambient-green  { --pt-ambient-color: var(--pt-color-success); }
.pt-ambient-amber  { --pt-ambient-color: var(--pt-color-warning); }
.pt-ambient-slate  { --pt-ambient-color: var(--pt-fg-tertiary); }

/* The user chip, its dropdown and the dropdown items used to be declared here
   AND in NavMenu.razor.css, with different values for the same properties — a
   26px avatar here against 1.75rem there, a 10px dropdown radius against
   --pt-radius-lg, a --pt-bg-elev-2 hover against --pt-bg-tint. Scoped CSS
   carries an attribute selector and therefore always won, so this copy was dead
   weight that still read as authoritative. The chip lives with its markup now;
   NavMenu.razor.css is the only definition. */

/* ═══════════════════════════════════════════════
   Collapsible raw data section (PollHistoryTable, which has no scoped sheet)
═══════════════════════════════════════════════ */
.pt-collapsible-header {
    display: flex;
    align-items: center;
    gap: var(--pt-space-2);
    cursor: pointer;
    user-select: none;
    padding: var(--pt-space-2) 0;
    color: var(--pt-fg-muted);
    font-size: var(--pt-text-sm);
    font-weight: var(--pt-weight-medium);
    border: none;
    background: none;
    font-family: inherit;
}

.pt-collapsible-header:hover {
    color: var(--pt-fg-body);
}

/* ═══════════════════════════════════════════════
   Loading skeleton variants
═══════════════════════════════════════════════ */
.pt-skeleton-stack {
    display: flex;
    flex-direction: column;
    gap: var(--pt-space-4);
}

/* Mirrors .pt-route-card's geometry — a skeleton that settles into a different
   shape than the thing it stood in for is a layout shift with extra steps. */
.pt-skeleton-card {
    background: var(--pt-bg-surface);
    border-radius: var(--pt-radius-lg);
    border: 1px solid var(--pt-border-subtle);
    border-left: 5px solid var(--pt-border-subtle);
    box-shadow: var(--pt-elev-2);
    padding: var(--pt-space-4) var(--pt-space-5);
    display: flex;
    flex-direction: column;
    gap: var(--pt-space-2);
}

.pt-skeleton-line {
    background: linear-gradient(90deg, var(--pt-border-subtle) 25%, var(--pt-bg-sunken) 50%, var(--pt-border-subtle) 75%);
    background-size: 200% 100%;
    animation: pt-shimmer 1.4s infinite;
    border-radius: var(--pt-radius-sm);
    height: 0.9rem;
}

.pt-skeleton-line.lg { height: 1.4rem; }
.pt-skeleton-line.w-70 { width: 70%; }
.pt-skeleton-line.w-50 { width: 50%; }
.pt-skeleton-line.w-40 { width: 40%; }
.pt-skeleton-line.w-30 { width: 30%; }

/* A `.pt-sparkline` / `.pt-sparkline svg` / `.pt-sparkline-label` block used to
   sit here, pinning the chart to 120×28. No element ever carried those classes:
   TrendSparkline renders .pt-sparkline-wrap and .pt-sparkline-svg, both styled
   in its own scoped sheet, and -label was declared in both places. */

/* ═══════════════════════════════════════════════
   #8: Chart hover tooltip.

   Created by pt-chart.js rather than by Razor markup, so it carries no scoped-CSS
   attribute and cannot live in TrendChart.razor.css. Positioned against
   .pt-trend-host, which supplies the containing block.
═══════════════════════════════════════════════ */
.pt-chart-tip {
    position: absolute;
    top: 8px;
    pointer-events: none;
    opacity: 0;
    transition: opacity var(--pt-dur-fast) var(--pt-ease);
    background: var(--pt-glass-bg-strong);
    backdrop-filter: blur(var(--pt-glass-blur));
    border: 1px solid var(--pt-border-subtle);
    border-radius: var(--pt-radius-sm);
    box-shadow: var(--pt-elev-2);
    padding: var(--pt-space-2) var(--pt-space-3);
    font-size: var(--pt-text-xs);
    line-height: var(--pt-leading-normal);
    color: var(--pt-fg-body);
    white-space: nowrap;
    z-index: var(--pt-z-pop);
}

.pt-chart-tip.visible { opacity: 1; }

.pt-chart-tip strong {
    color: var(--pt-fg-primary);
    font-variant-numeric: tabular-nums;
}

.pt-chart-tip-flag {
    color: var(--pt-color-danger);
    font-weight: var(--pt-weight-semibold);
}

/* ═══════════════════════════════════════════════
   #6: Connectivity indicators. The polling base stands down while a tab is
   hidden or offline; these tell the user why the numbers stopped moving.
═══════════════════════════════════════════════ */
.pt-status-offline {
    color: var(--pt-color-warning);
    font-weight: var(--pt-weight-medium);
}

.pt-stale-banner {
    align-items: center;
    background: var(--pt-bg-tint);
    border: 1px solid var(--pt-border-subtle);
    border-left: 3px solid var(--pt-color-warning);
    border-radius: var(--pt-radius-sm);
    color: var(--pt-fg-secondary);
    display: flex;
    font-size: var(--pt-text-sm);
    gap: var(--pt-space-2);
    margin-bottom: var(--pt-space-4);
    padding: var(--pt-space-2) var(--pt-space-3);
}

.pt-stale-banner .pt-icon { color: var(--pt-color-warning); }

/* ═══════════════════════════════════════════════
   Empty states. Shared by the dashboard and the route-comparison page (#8),
   so these live here rather than in either page's scoped stylesheet.
═══════════════════════════════════════════════ */
.pt-empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 52vh;
    text-align: center;
    padding: var(--pt-space-12) var(--pt-space-4);
    background: var(--pt-glass-bg);
    backdrop-filter: blur(var(--pt-glass-blur)) saturate(var(--pt-glass-saturate));
    -webkit-backdrop-filter: blur(var(--pt-glass-blur)) saturate(var(--pt-glass-saturate));
    border: 1px solid var(--pt-glass-border);
    border-radius: var(--pt-radius-lg);
}

/* Inside a card that already has its own surface and border — a chart with no
   data, say. Half the page height and no second frame around it. */
.pt-empty-state-inline {
    min-height: 0;
    padding: var(--pt-space-8) var(--pt-space-4);
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border: 0;
}

/* Was an emoji at --pt-text-3xl. Emoji ignore `color`, so the glyph stayed
   full-saturation against a muted empty state and drew the eye away from the
   sentence that actually says what to do next. */
.pt-empty-state-icon {
    color: var(--pt-fg-faint);
    margin-bottom: var(--pt-space-4);
}

.pt-empty-state-title {
    font-size: var(--pt-text-xl);
    font-weight: var(--pt-weight-semibold);
    color: var(--pt-fg-primary);
    margin-bottom: var(--pt-space-2);
}

.pt-empty-state-text {
    color: var(--pt-fg-secondary);
    margin-bottom: var(--pt-space-6);
    max-width: 34rem;
}

/* #10 — the secondary "try a sample route" path, deliberately quieter than the
   primary call to action next to it. */
.pt-empty-state-alt {
    border-top: 1px solid var(--pt-border-subtle);
    margin-top: var(--pt-space-6);
    padding-top: var(--pt-space-5);
}

.pt-empty-state-note {
    color: var(--pt-fg-tertiary);
    font-size: var(--pt-text-xs);
    margin-top: var(--pt-space-2);
    max-width: 26rem;
}

.pt-demo-banner {
    align-items: center;
    background: var(--pt-bg-tint);
    border: 1px dashed var(--pt-color-info);
    border-radius: var(--pt-radius-md);
    display: flex;
    flex-wrap: wrap;
    gap: var(--pt-space-3);
    padding: var(--pt-space-3) var(--pt-space-4);
}

.pt-demo-banner-tag {
    color: var(--pt-color-info);
    font-size: var(--pt-text-xs);
    font-weight: var(--pt-weight-bold);
    letter-spacing: 0.06em;
    white-space: nowrap;
}

.pt-demo-banner-text {
    color: var(--pt-fg-secondary);
    flex: 1 1 18rem;
    font-size: var(--pt-text-sm);
    min-width: 0;
}

/* ── Section headings inside Cards
   Used by every section title inside a Card. Visually we keep the small,
   weighty treatment from when these were rendered at H6; structurally we
   re-emit them at H3 so the heading-list skips an empty H2/H4/H5 on every
   page that uses them. The rule applies the font treatment; the level-bumping
   has already happened in the markup. */
.pt-section-heading {
    font-size: var(--pt-text-md);
    line-height: 1.3;
    font-weight: var(--pt-weight-semibold);
    color: var(--pt-color-fg);
    letter-spacing: -0.005em;
    margin-top: 0;
}

/* ═══════════════════════════════════════════════
   Route probe status strip
   Lives between the page header and the answer card. Three cells in one row:
     • refresh-in-flight pulse   • last Google Maps probe time   • cadence / count.
   Designed to read at a glance without competing with the headline departure.
══════════════════════════════════════════════ */
.pt-probe-status {
    display: flex;
    align-items: center;
    gap: var(--pt-space-4);
    padding: var(--pt-space-2) var(--pt-space-4);
    background: var(--pt-bg-surface);
    border-radius: var(--pt-radius-md);
    border: 1px solid var(--pt-border-subtle);
    box-shadow: var(--pt-elev-1);
    margin-bottom: var(--pt-space-6);
    flex-wrap: wrap;
}

.pt-probe-cell {
    display: inline-flex;
    align-items: center;
    gap: var(--pt-space-2);
    font-size: var(--pt-text-sm);
    color: var(--pt-fg-secondary);
    min-width: 0;       /* allow flex-shrink so long "X min ago" wraps don't push the row off-screen */
}

.pt-probe-cell-refresh { flex: 0 0 auto; }

.pt-probe-cell-lastprobe,
.pt-probe-cell-cadence {
    flex: 1 1 auto;
    min-width: 0;
}

.pt-probe-actions {
    margin-left: auto;
    flex: 0 0 auto;
}

.pt-probe-icon {
    color: var(--pt-fg-tertiary);
    flex-shrink: 0;
}

.pt-probe-label {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.pt-probe-sep {
    margin: 0 var(--pt-space-2);
    color: var(--pt-fg-tertiary);
}

.pt-probe-dot {
    width: 0.5rem;
    height: 0.5rem;
    border-radius: var(--pt-radius-full);
    display: inline-block;
    flex-shrink: 0;
}

.pt-probe-dot-green { background: var(--pt-color-success); }
.pt-probe-dot-amber { background: var(--pt-color-warning); }

/* Pulse while a refresh is in flight — the same visual vocabulary as
   progress_activity, so the two never appear together without it being obvious
   which is which. */
.pt-probe-dot-pulse {
    background: var(--pt-color-brand-500);
    animation: pt-probe-pulse 1.2s ease-in-out infinite;
}

@keyframes pt-probe-pulse {
    0%, 100% { transform: scale(1);   opacity: 1; }
    50%      { transform: scale(1.6); opacity: 0.35; }
}

/* Reduced-motion: drop the pulse animation, keep the static dot — the
   "Refreshing…" text already conveys the state, the dot is decoration. */
@media (prefers-reduced-motion: reduce) {
    .pt-probe-dot-pulse { animation: none; }
}

/* ── Route map ────────────────────────────────────────────────────────────
   Leaflet ships its own stylesheet (lib/leaflet/leaflet.css), which is loaded
   unlayered alongside this file. Everything below either sizes the container —
   Leaflet refuses to draw into a zero-height element — or restyles the two
   chrome pieces that would otherwise look pasted on from another app. */

.pt-map-frame {
    border-radius: var(--pt-radius-md);
    overflow: hidden;
    border: 1px solid var(--pt-border-subtle);
    background: var(--pt-bg-sunken);
}

.pt-map-canvas {
    /* Leaflet measures the element, so the height must be a real length, never
       a percentage of an auto-height parent. Short on phones, taller once there
       is room, so the map never eats the whole first screen. */
    height: 16rem;
    width: 100%;
}

/* Sized by the COLUMN it is in, not the window. On route detail the evidence
   column on a 1440px desktop is narrower than the window by the width of the
   answer column, and a viewport query would have given the map a tall aspect
   ratio in a column too narrow to justify it. */
@container pt-main (min-width: 40rem) {
    .pt-map-canvas { height: 22rem; }
}

/* No container to query — the dashboard and other pages that host a map are not
   inside .pt-route-evidence. Falls back to the viewport there. */
@supports not (container-type: inline-size) {
    @media (min-width: 40rem) {
        .pt-map-canvas { height: 22rem; }
    }
}

/* Leaflet's default panes are white, which flashes bright in dark mode before
   the first tiles paint and shows through at the edges of the world. */
.pt-map-canvas.leaflet-container {
    background: var(--pt-bg-sunken);
    font: inherit;
}

.pt-map-canvas .leaflet-control-attribution {
    background: color-mix(in srgb, var(--pt-bg-surface) 88%, transparent);
    color: var(--pt-fg-muted);
    font-size: 0.6875rem;
}

.pt-map-canvas .leaflet-control-attribution a { color: var(--pt-fg-secondary); }

.pt-map-canvas .leaflet-bar a {
    background: var(--pt-bg-surface);
    color: var(--pt-fg-primary);
    border-bottom-color: var(--pt-border-subtle);
}

.pt-map-canvas .leaflet-bar a:hover { background: var(--pt-bg-elev-2); }

/* Endpoint pins. A divIcon rather than Leaflet's default sprite so the two ends
   are told apart by shape-and-fill, and so they follow the theme. */
.pt-map-pin-dot {
    display: block;
    width: 0.875rem;
    height: 0.875rem;
    border-radius: var(--pt-radius-full);
    border: 2px solid var(--pt-bg-surface);
    box-shadow: 0 1px 4px rgb(var(--pt-shadow-rgb) / 0.45);
}

.pt-map-pin-origin .pt-map-pin-dot { background: var(--pt-fg-primary); }
.pt-map-pin-dest .pt-map-pin-dot   { background: var(--pt-color-brand-500); }

/* Legend — the sentence that makes the colour mean something. */
.pt-map-legend {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 0.625rem;
    font-size: 0.875rem;
    color: var(--pt-fg-body);
}

.pt-map-legend-swatch {
    width: 1.25rem;
    height: 0.25rem;
    border-radius: var(--pt-radius-full);
    align-self: center;
    flex-shrink: 0;
}

.pt-map-swatch-clear   { background: var(--pt-traffic-clear); }
.pt-map-swatch-normal  { background: var(--pt-traffic-normal); }
.pt-map-swatch-slow    { background: var(--pt-traffic-slow); }
.pt-map-swatch-heavy   { background: var(--pt-traffic-heavy); }
.pt-map-swatch-unknown { background: var(--pt-traffic-unknown); }

.pt-map-legend-note {
    color: var(--pt-fg-muted);
    font-size: 0.8125rem;
}

.pt-map-empty {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 1.5rem;
    border-radius: var(--pt-radius-md);
    border: 1px dashed var(--pt-border-subtle);
    color: var(--pt-fg-muted);
    font-size: 0.875rem;
}

/* ═══════════════════════════════════════════════
   FX layers — the canvases the effects draw into
   ═══════════════════════════════════════════════

   All three are inert decoration: no pointer events, aria-hidden in markup, and
   removed entirely when the motion level is "off". They are positioned rather
   than laid out, so none of them can move a single pixel of real content. */

/* The living gradient. Behind everything, including the page background, which is
   why <body> must not paint an opaque colour over it — see the rule below. */
.pt-ambient {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: -1;
    pointer-events: none;
    /* Its own compositor layer: the shader repaints 30 times a second and must never
       drag the page's paint along with it. */
    will-change: opacity;
    contain: strict;
}

/* Getting the wash BEHIND the page without losing the page colour.

   A z-index:-1 child of <body> paints above the canvas background but below every
   block background, so <body> must not paint one. It cannot simply be made
   transparent though: with <html> also transparent the browser propagates body's
   background up to the canvas, and "transparent" propagated is a white page in dark
   mode. So the page colour moves to <html>, which stops the propagation and becomes
   the ground the wash sits on. */
:root:has(.pt-ambient) {
    background: var(--pt-bg-page);
}

:root:has(.pt-ambient) body {
    background: transparent;
}

/* Confetti. Above the page, below the toasts, and never in the way of a tap. */
.pt-particles {
    position: fixed;
    inset: 0;
    z-index: calc(var(--pt-z-toast) - 1);
    pointer-events: none;
}

/* The flow particles on the map. Leaflet positions this inside its overlay pane;
   only the stacking and the pointer behaviour are ours. */
.pt-flow-canvas {
    position: absolute;
    top: 0;
    left: 0;
    pointer-events: none;
    z-index: 350;   /* above tiles (200) and the route line, below markers (600) */
}

/* Motion off: nothing decorative renders at all. Belt and braces — the JS already
   declines to create these — so a stale canvas from a previous setting cannot linger. */
[data-motion="off"] .pt-ambient,
[data-motion="off"] .pt-particles,
[data-motion="off"] .pt-flow-canvas {
    display: none;
}

/* ═══════════════════════════════════════════════
   Glass pass
   ═══════════════════════════════════════════════

   Applied only to surfaces that FLOAT over content — the header, the bottom nav,
   the toasts. Cards do not get it: a card is where text lives, and text on a
   translucent pane over a moving gradient is the classic accessibility failure of
   this style. The tokens already collapse to solid under forced-colors and
   prefers-contrast, so those users get flat surfaces automatically. */

.app-header {
    background: var(--pt-glass-bg-strong);
    backdrop-filter: blur(var(--pt-glass-blur)) saturate(var(--pt-glass-saturate));
    -webkit-backdrop-filter: blur(var(--pt-glass-blur)) saturate(var(--pt-glass-saturate));
    border-bottom: 1px solid var(--pt-glass-border);
}

/* Cards get a whisper of the same treatment: still fully opaque behind text, but
   with the saturation lift and the hairline that make the set read as glass rather
   than as flat rectangles. */
.rz-card,
.pt-skeleton-card {
    border: 1px solid var(--pt-glass-border);
    box-shadow: var(--pt-elev-2), inset 0 1px 0 light-dark(rgb(255 255 255 / 0.6), rgb(255 255 255 / 0.04));
}

/* No backdrop-filter support: the tokens are already translucent, which over a
   moving gradient would be unreadable. Fall back to the solid surface. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .app-header {
        background: var(--pt-bg-surface);
    }
}

/* ═══════════════════════════════════════════════
   Loading shimmer
   ═══════════════════════════════════════════════

   The skeleton lines already shimmered. What they lacked was a HIGHLIGHT — a
   specular sweep travelling across the whole card rather than each line animating
   in isolation, which is what made three lines look like three separate loaders
   instead of one surface waiting to be filled. */

.pt-skeleton-card {
    position: relative;
    overflow: hidden;
}

.pt-skeleton-card::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(
        105deg,
        transparent 30%,
        light-dark(rgb(255 255 255 / 0.55), rgb(255 255 255 / 0.07)) 48%,
        transparent 66%);
    transform: translateX(-100%);
    animation: pt-sheen 1.9s var(--pt-ease) infinite;
    pointer-events: none;
}

/* Staggered so the three cards sweep in sequence, reading as one wave down the
   list rather than three synchronised flashes. */
.pt-skeleton-stack .pt-skeleton-card:nth-child(2)::after { animation-delay: 0.18s; }
.pt-skeleton-stack .pt-skeleton-card:nth-child(3)::after { animation-delay: 0.36s; }

@keyframes pt-sheen {
    to { transform: translateX(100%); }
}

/* The progress line gets the same specular treatment, so a page that is loading
   looks like the cards that are loading. */
.pt-progress-line {
    position: relative;
    overflow: hidden;
}

/* Reduced motion, or motion switched off: the shimmer and the sheen both stop. The
   skeleton still communicates "loading" by being a skeleton. */
@media (prefers-reduced-motion: reduce) {
    .pt-skeleton-line { animation: none; }
    .pt-skeleton-card::after { animation: none; opacity: 0; }
}

[data-motion="off"] .pt-skeleton-line { animation: none; }
[data-motion="off"] .pt-skeleton-card::after { animation: none; opacity: 0; }

/* ═══════════════════════════════════════════════
   Sample route markers
   ═══════════════════════════════════════════════ */

/* Quiet by design. It has to be legible enough that nobody mistakes generated data
   for measured data, and plain enough that it never competes with the answer. */
.pt-sample-chip {
    background: var(--pt-bg-tint-strong);
    border-radius: var(--pt-radius-full);
    color: var(--pt-fg-muted);
    font-size: var(--pt-text-xs);
    font-weight: var(--pt-weight-semibold);
    letter-spacing: 0.02em;
    margin-inline-start: var(--pt-space-2);
    padding: 0.1rem 0.45rem;
    text-transform: uppercase;
    white-space: nowrap;
}

.pt-sample-banner {
    align-items: center;
    background: var(--pt-bg-tint);
    border: 1px solid var(--pt-border-subtle);
    border-radius: var(--pt-radius-md);
    color: var(--pt-fg-body);
    display: flex;
    font-size: var(--pt-text-sm);
    gap: var(--pt-space-2);
    line-height: var(--pt-leading-normal);
    margin-bottom: var(--pt-space-3);
    padding: var(--pt-space-3);
}

/* The empty state had one button and stacked naturally; with two it needs to wrap
   rather than push the secondary action off a narrow screen. */
.pt-empty-state-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--pt-space-2);
    justify-content: center;
}

/* ═══════════════════════════════════════════════
   Weather impact
   ═══════════════════════════════════════════════ */

.pt-weather {
    margin-top: var(--pt-space-4);
}

.pt-weather-lede {
    color: var(--pt-fg-body);
    font-size: var(--pt-text-sm);
    line-height: var(--pt-leading-normal);
    margin: 0 0 var(--pt-space-3);
    max-width: var(--pt-measure);
}

.pt-weather-rows {
    display: flex;
    flex-direction: column;
    gap: var(--pt-space-2);
    list-style: none;
    margin: 0;
    padding: 0;
}

/* Four columns on a comfortable viewport: label, bar, delta, sample count. The bar
   column is the flexible one so the numbers stay in their own aligned tracks. */
.pt-weather-row {
    align-items: center;
    display: grid;
    gap: var(--pt-space-2);
    grid-template-columns: 7rem 1fr auto;
    grid-template-areas: "name track delta" "count count count";
}

@container pt-main (min-width: 34rem) {
    .pt-weather-row {
        grid-template-columns: 7rem 1fr 4.5rem 9rem;
        grid-template-areas: "name track delta count";
    }
}

.pt-weather-name {
    align-items: center;
    color: var(--pt-fg-body);
    display: inline-flex;
    font-size: var(--pt-text-sm);
    font-weight: var(--pt-weight-medium);
    gap: 0.35rem;
    grid-area: name;
}

/* The track is the baseline: its centre line is "the usual time for this slot", and a
   bar grows out from there in the direction of the effect. */
.pt-weather-track {
    background: var(--pt-bg-sunken);
    border-radius: var(--pt-radius-full);
    display: block;
    grid-area: track;
    height: 0.5rem;
    position: relative;
}

.pt-weather-track::before {
    background: var(--pt-border-strong);
    content: "";
    inset-block: -0.15rem;
    inset-inline-start: 50%;
    position: absolute;
    width: 1px;
}

.pt-weather-bar {
    border-radius: var(--pt-radius-full);
    height: 100%;
    position: absolute;
    top: 0;
    transition: width var(--pt-dur-base) var(--pt-ease);
}

.pt-weather-bar.is-slower {
    background: var(--pt-traffic-slow);
    inset-inline-start: 50%;
}

.pt-weather-bar.is-faster {
    background: var(--pt-traffic-clear);
    /* Grows leftwards from the centre — right edge pinned, width extends back. */
    inset-inline-end: 50%;
}

.pt-weather-delta {
    color: var(--pt-fg-strong);
    font-size: var(--pt-text-sm);
    font-variant-numeric: tabular-nums;
    font-weight: var(--pt-weight-semibold);
    grid-area: delta;
    text-align: end;
}

/* A number built on too few samples is shown, but never with the visual authority of
   one that has earned it. */
.pt-weather-delta.is-provisional {
    color: var(--pt-fg-muted);
    font-weight: var(--pt-weight-regular);
}

.pt-weather-count {
    color: var(--pt-fg-muted);
    font-size: var(--pt-text-xs);
    grid-area: count;
    text-align: start;
}

@container pt-main (min-width: 34rem) {
    .pt-weather-count { text-align: end; }
}

.pt-weather-note {
    color: var(--pt-fg-muted);
    font-size: var(--pt-text-xs);
    margin: var(--pt-space-3) 0 0;
    max-width: var(--pt-measure);
}

/* ═══════════════════════════════════════════════
   Wide layouts (route detail)
   ═══════════════════════════════════════════════

   .pt-split and .pt-split-main used to live here, for the route rail beside the
   route page. Route detail is .pt-route-two now — the answer and its controls
   on the left, the evidence on the right — and nothing else ever used the
   split, so only the width allowance it needed remains. */

@media (min-width: 1200px) {
    /* Two columns need more room than the reading-width cap allows. Prose still
       gets its measure from --pt-measure inside each column; this only lets the
       pair sit side by side without squeezing either.

       It lives here rather than in RouteDetailPage.razor.css because .content
       is MainLayout's element and never carries another component's scope
       attribute — the same trap .app-nav-link documents above. */
    .content:has(.pt-route-two) {
        max-width: 1440px;
    }
}

/* ═══════════════════════════════════════════════
   Child-component overrides for route detail's two-column layout.

   Every rule below targets markup owned by a CHILD component — RouteMap,
   RouteProbeStatus — so it cannot live in RouteDetailPage.razor.css: scoped CSS
   stamps its attribute only on the elements written in its own .razor file. Same
   trap NavMenu.razor.css documents for .app-nav-link.
   ═══════════════════════════════════════════════ */

/* Beside the chart rather than above it, the map is a shape, not a document.
   22rem inside a wide column made the evidence row taller than the fold. */
@container pt-main (min-width: 44rem) {
    .pt-route-evidence-top .pt-map-canvas { height: 15rem; }
}

/* A header carrying the probe chips is sharing its row with about 30rem of live
   status, so the title gets one step down the scale. Without this, a two-address
   title ("1 Apple Park Way → Googleplex") wrapped to three lines at --pt-text-2xl
   and pushed the whole page down before it had said anything. */
.pt-page-header:has(.pt-probe-chips) .pt-page-title {
    font-size: var(--pt-text-xl);
}

/* And the chips are capped so they wrap to a second line instead of taking the
   room the title needs. Status yields to identity, not the other way round. */
.pt-probe-chips {
    max-width: 32rem;
}

/* Radzen's alert is sized for a full-width card. In the answer column it is a
   status line, and its default padding made "Monitoring active" the tallest
   block on the page. */
.pt-route-side .rz-alert {
    padding: var(--pt-space-2) var(--pt-space-3);
    font-size: var(--pt-text-sm);
}

/* The probe signals as header chips: same three cells, no card, no row of its
   own. Wraps onto a second line before it will squeeze the page title. */
.pt-probe-chips {
    display: flex;
    align-items: center;
    gap: var(--pt-space-3);
    flex-wrap: wrap;
    justify-content: flex-end;
}

.pt-probe-chips .pt-probe-cell {
    font-size: var(--pt-text-xs);
    color: var(--pt-fg-secondary);
}

/* In the strip the button is pushed to the far end by margin-left:auto. In the
   header the chips are already right-aligned, so it just follows the last one. */
.pt-probe-chips .pt-probe-actions {
    margin-left: 0;
}

/* The header's action slot is the narrowest place these chips ever render, and
   a wrapping "5 min ago" is worth more than a truncated one. */
.pt-probe-chips .pt-probe-label {
    white-space: normal;
}

/* ═══════════════════════════════════════════════
   Measure — prose line length
   ═══════════════════════════════════════════════

   Body copy that runs the full width of a 1440px shell is measurably harder to
   read: the eye loses the line on the return sweep. These are the blocks that
   are genuinely prose; tables, charts and card grids are exempt and want every
   pixel they can get. */

.pt-empty-state-text,
.pt-map-legend,
.pt-settings-meta {
    max-width: var(--pt-measure);
}

.pt-empty-state-text {
    /* Centred blocks need the margin too, or the cap just pushes them left. */
    margin-inline: auto;
}

/* ═══════════════════════════════════════════════
   Route card actions
   ═══════════════════════════════════════════════ */

/* Sits at the card's trailing edge, quiet until the card is hovered or something inside
   it takes focus — but always in the tab order, because a control that only appears on
   hover is a control a keyboard user does not have. */
.pt-route-card-actions {
    display: flex;
    gap: var(--pt-space-1);
    justify-content: flex-end;
    margin-top: var(--pt-space-2);
    opacity: 0.55;
    transition: opacity var(--pt-dur-fast) var(--pt-ease);
}

.pt-route-card:hover .pt-route-card-actions,
.pt-route-card-actions:focus-within {
    opacity: 1;
}

@media (hover: none) {
    /* No hover to reveal them on touch, where the swipe gestures are the primary path. */
    .pt-route-card-actions { opacity: 1; }
}
