/*
 * Tournal — the one screen stylesheet (SPEC §25.8, §26.1).
 *
 * Everything below is built out of the tokens in `:root`. A rule that needs a
 * size, a radius, a weight, a shadow or a duration takes a token; a raw value in
 * a rule is what `tests/Accessibility/StylesheetContractTest.php` fails on,
 * because fourteen unrelated font sizes is how a stylesheet stops being a system
 * (ADR-0063).
 *
 * Two further contracts the suite holds:
 *
 * - every class a template uses has a rule here, and every class here is used by
 *   a template (`StylesheetContractTest`). A class with no rule is a design that
 *   was never written, and it is how a sign-in page came to be laid out in three
 *   columns;
 * - every colour is a custom property on `:root`, so
 *   `tests/Accessibility/ContrastTest.php` can measure every pair that occurs
 *   rather than sample a screenshot.
 *
 * `public/assets/css/report.css` is the other stylesheet and shares nothing with
 * this one on purpose: a report is printed, not looked at on a screen (§27.4).
 *
 * Hand-written CSS, no preprocessor, no remote font, no build step (§4.2, §6.1).
 */

:root {
    /* == Colour, layer 1: the primitives ==================================== */
    /*
     * The only place in this file where a colour is written down (ADR-0073).
     * Nothing outside `:root` names one, and no `--color-*` token below holds
     * one — every semantic token points at a primitive instead.
     *
     * That split is what makes a second colour mode cost nothing in the rules:
     * a mode re-points the semantic layer and no component learns that modes
     * exist. It is also what keeps
     * `ContrastTest::testTheStylesheetDeclaresNoColourOutsideThePalette`
     * working unchanged, because every literal it can find is still declared
     * here where the contrast check reads it.
     *
     * The neutrals carry a slight blue cast (hue ≈ 220) rather than being
     * true greys. Warm neutrals under a translucent surface turn muddy, and
     * the navigation bar is translucent on every screen.
     */

    --frost-0: #ffffff;
    --frost-50: #f7f9fc;
    --frost-100: #f4f6fa;
    --frost-150: #edf1f7;
    --frost-200: #e7ecf3;
    --frost-300: #d5dde8;
    --frost-400: #a7b3c3;
    --frost-450: #8b98a9;
    --frost-500: #79879b;
    --frost-600: #5c6a7d;
    --frost-700: #39434f;
    --frost-750: #2f3a48;
    --frost-800: #252d39;
    --frost-900: #161c24;
    --frost-950: #0e1319;
    --frost-1000: #05080c;

    --blue-100: #eaf2fd;
    --blue-200: #8fc7ff;
    --blue-300: #4ca6ff;
    --blue-500: #0d68d0;
    --blue-700: #17499f;
    --blue-800: #0b3f9e;
    --blue-900: #15304c;

    --red-300: #ff8a90;
    --red-600: #a4232b;
    --amber-300: #ffb443;
    --amber-700: #8a5300;
    --green-300: #4fd9a0;
    --green-700: #1d6b3f;

    /* == Colour, layer 2: the semantic tokens ================================ */
    /*
     * The working layer, and the only one a rule may name (§26.1: a light
     * background, limited accent colours). Every value here is a `var()`.
     */

    --color-background: var(--frost-100);
    --color-surface: var(--frost-0);
    --color-surface-muted: var(--frost-150);
    /* One step below the muted surface: the track a segmented control sits in
       and the quiet cells of the calendar, where a fill has to read as
       *behind* rather than *beside* (§26.1). */
    --color-surface-sunken: var(--frost-200);
    --color-text: var(--frost-950);
    --color-text-muted: var(--frost-600);
    /* Dividers and panel edges: decoration, so no contrast threshold applies. */
    --color-border: var(--frost-300);
    /* The visible boundary of a control the user operates — a field, a
       secondary button, a switch. WCAG 1.4.11 asks for 3:1 against every
       surface it can sit on, and `tests/Accessibility/ContrastTest.php`
       measures it (§25.8, §26.1). */
    --color-border-strong: var(--frost-500);
    /*
     * Azure-ward of the royal blue this app used to carry, which is the one
     * change of identity in the palette. It stops there rather than at the
     * brighter azure of the reference system: that one measures 4.03:1 on a
     * white panel and 3.73:1 on the page, and §25.8 asks for 4.5:1. This is
     * 5.37:1 and 4.97:1 — the direction taken as far as the threshold allows.
     */
    --color-accent: var(--blue-500);
    /* The same action under a pointer. One step darker, and dark enough that a
       white label on it still meets 4.5:1 (ADR-0070). */
    --color-accent-strong: var(--blue-700);
    --color-accent-contrast: var(--frost-0);
    /* The fill of a chosen segment or an active day. It carries text, so it is
       measured against the text, the accent and the control boundary — and it
       is a shade lighter than it looks like it needs to be, because the accent
       on it sits at 4.51:1 one step darker, which is a rounding error away
       from failing. */
    --color-accent-soft: var(--blue-100);
    --color-focus: var(--blue-800);
    /* Status colours never carry meaning alone; a word accompanies them (§26.1,
       §25.8). Three of them, so "needs attention" stops borrowing the muted
       grey that every secondary line already uses (ADR-0065). */
    --color-danger: var(--red-600);
    --color-warning: var(--amber-700);
    --color-success: var(--green-700);

    /* -- The material of the floating navigation ---------------------------- */
    /*
     * The one translucent surface in the application (§26.1). Its layers are
     * tokens rather than literals for the same reason the palette is: they are
     * the part of the design that has to change with the colour mode, and a
     * white tint written into the rule would have stayed white in the dark
     * (ADR-0073). Alpha is what these carry that a `--color-*` cannot, so they
     * are their own small group rather than semantic colours.
     */

    --glass-tint: rgba(255, 255, 255, 0.58);
    --glass-edge: rgba(255, 255, 255, 0.78);
    /* The light along the top inside edge, and its weaker reflex along the
       bottom. This pair is what gives the material a perceived thickness: a
       blurred, tinted, bordered surface without it reads as a blurred
       rectangle, and the difference is the whole of it (ADR-0074). */
    --glass-specular: rgba(255, 255, 255, 0.9);
    --glass-specular-low: rgba(255, 255, 255, 0.34);
    --glass-scrim-top: rgba(244, 246, 250, 0.28);
    --glass-scrim-bottom: rgba(244, 246, 250, 0.66);

    /* -- Type (ADR-0063) ---------------------------------------------------- */

    --text-xs: 0.75rem;
    --text-sm: 0.875rem;
    --text-md: 1rem;
    --text-lg: 1.125rem;
    --text-xl: 1.375rem;
    --text-2xl: 1.75rem;
    /* The numbers a driver opens the app for (§26.1). */
    --text-figure: 2rem;

    /*
     * Tracking, one step per size step (ADR-0076).
     *
     * A typeface is spaced for one size and set at seven, and the larger it is
     * set the looser it looks: at 28px the default spacing reads as gappy, at
     * 12px as tight. Negative above the body size and slightly positive below
     * it is optical compensation, not a style, and the amount is a function of
     * the size — which is exactly why it belongs beside the size scale.
     *
     * It was the last type property still written as literals. Nine rules
     * carried six different values, three sizes carried three different
     * trackings each, and `testTypeAndRadiusComeFromTheTokenScale` never looked
     * at `letter-spacing`. It does now.
     */

    --track-figure: -0.019em;
    --track-2xl: -0.017em;
    --track-xl: -0.012em;
    --track-lg: -0.006em;
    --track-sm: 0.002em;
    --track-xs: 0.004em;

    --weight-body: 400;
    --weight-label: 600;
    --weight-title: 700;

    --leading-tight: 1.2;
    --leading-normal: 1.5;

    /* -- Space -------------------------------------------------------------- */

    --space-xs: 0.25rem;
    --space-sm: 0.5rem;
    --space-md: 1rem;
    --space-lg: 1.5rem;
    --space-xl: 2rem;
    --space-2xl: 3rem;

    /* -- Flow: the three gaps a container may put between its children ------ */
    /*
     * Space belongs to the container, never to the child. Before this, every
     * component carried its own `margin-bottom` and several carried none, so
     * the gap between two blocks was whatever the first one happened to
     * declare: measured across the application it was −16, 0, 8, 16, 24, 40 and
     * 48 pixels, and the zeros were two paragraphs touching and a button row
     * sitting on the panel below it (ADR-0068).
     */

    /* a heading and the block it introduces */
    --flow-tight: var(--space-sm);
    /* two blocks inside one surface */
    --flow: var(--space-md);
    /* two regions of a page */
    --flow-loose: var(--space-lg);

    /* -- The two content edges ---------------------------------------------- */
    /*
     * The page inset, and the surface inset on top of it. A line of text starts
     * at one of exactly two places, and the two mean different things: "this is
     * the page" and "this is on a surface" (ADR-0069).
     */
    --page-pad: var(--space-md);
    --surface-pad: var(--space-md);

    /* -- Radius: four steps, each with a job -------------------------------- */

    /* tags and flags */
    --radius-sm: 8px;
    /* controls: inputs, buttons */
    --radius-md: 12px;
    /* surfaces: panels, the floating navigation */
    --radius-lg: 18px;
    /* segmented controls and switcher pills */
    --radius-pill: 999px;

    /* -- Elevation: a ramp of four, and no more (§26.1) --------------------- */
    /*
     * Each step is a tight contact shadow plus a soft ambient one, because a
     * single soft shadow disappears against a near-white page — which is what
     * the floating navigation's did: 12 % at 28 pixels of blur under a
     * translucent white bar was very nearly nothing (ADR-0071).
     *
     * A shadow is the ink of the page at low alpha, so both are the RGB of a
     * primitive above and both went cold when the ink did: `--frost-950` is a
     * blue-cast near-black, and a neutral-black shadow under a blue-cast
     * surface reads as dirt rather than as depth (ADR-0073).
     */

    /* A panel on the page. Its hairline does the separating. */
    --elev-0: none;
    /* A control lifted off its own track — the chosen segment of a switcher. */
    --elev-1: 0 1px 2px rgba(14, 19, 25, 0.09);
    /* A surface floating over content: the navigation bar, and only that. */
    --elev-2: 0 1px 3px rgba(14, 19, 25, 0.12), 0 10px 28px rgba(14, 19, 25, 0.16);
    /* The central action, tinted by its own colour (§25.2). */
    --elev-3: 0 2px 6px rgba(13, 104, 208, 0.42), 0 10px 26px rgba(13, 104, 208, 0.5);

    /* -- Motion: applied in four places, removed by the reduced-motion block - */

    --duration-fast: 120ms;
    --duration-base: 200ms;
    --ease-standard: cubic-bezier(0.2, 0, 0, 1);

    /* -- Measure and layout ------------------------------------------------- */

    /* The single column a phone reads. */
    --content-max: 44rem;
    /* Wide viewports get structure, not stretch (ADR-0066). */
    --content-max-wide: 60rem;
    /* A line of prose stops here, whatever the column does. */
    --measure: 36rem;
    /* And a form control stops here, so a desktop never stretches an input. */
    --measure-control: 26rem;

    --nav-height: 4.25rem;
    --touch-target: 44px;

    --font-stack: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}

/* == Colour, layer 3: the dark mode ========================================= */
/*
 * The same semantic names, re-pointed at darker primitives (ADR-0073). Not one
 * rule below knows this block exists, which is the whole return on the three
 * layers: a mode costs twenty-three declarations and no component changes.
 *
 * Light is the authored mode and the fallback. Dark follows the system setting,
 * and `[data-theme="light"]` on the root turns it off again — the hook a later
 * round needs if the owner ever wants a switch in App Settings. Nothing sets
 * that attribute today, and nothing here needs JavaScript (ADR-0001).
 *
 * Three inversions are deliberate and are not typing mistakes:
 *
 * - `--color-surface-muted` becomes *lighter* than `--color-surface`, because a
 *   row under a pointer lifts towards the light in both modes;
 * - `--color-accent-contrast` becomes near-black, because white on this
 *   accent is 2.3:1 and would fail §25.8 on every primary button;
 * - `--color-accent-strong` becomes *lighter* than `--color-accent`, for the
 *   same reason as the first.
 *
 * The elevation ramp is re-declared rather than inherited: a near-black shadow
 * on a near-black page separates nothing. In the dark the alphas roughly double
 * and the bar's bright edge does the work the shadow does in the light.
 *
 * The two blocks below must stay identical.
 * `ContrastTest::testBothDarkSelectorsDeclareTheSameThing` holds them to it,
 * because CSS has no way to put a media query and an attribute selector in one
 * selector list, and a palette that drifts between them would be a mode nobody
 * ever looks at.
 */

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --color-background: var(--frost-950);
        --color-surface: var(--frost-800);
        --color-surface-muted: var(--frost-750);
        --color-surface-sunken: var(--frost-900);
        --color-text: var(--frost-50);
        --color-text-muted: var(--frost-400);
        --color-border: var(--frost-700);
        --color-border-strong: var(--frost-450);
        --color-accent: var(--blue-300);
        --color-accent-strong: var(--blue-200);
        --color-accent-contrast: var(--frost-1000);
        --color-accent-soft: var(--blue-900);
        --color-focus: var(--blue-200);
        --color-danger: var(--red-300);
        --color-warning: var(--amber-300);
        --color-success: var(--green-300);

        --glass-tint: rgba(37, 45, 57, 0.56);
        --glass-edge: rgba(255, 255, 255, 0.16);
        --glass-specular: rgba(255, 255, 255, 0.24);
        --glass-specular-low: rgba(255, 255, 255, 0.08);
        --glass-scrim-top: rgba(5, 8, 12, 0.28);
        --glass-scrim-bottom: rgba(5, 8, 12, 0.66);

        --elev-1: 0 1px 2px rgba(5, 8, 12, 0.4);
        --elev-2: 0 1px 3px rgba(5, 8, 12, 0.5), 0 10px 28px rgba(5, 8, 12, 0.6);
        --elev-3: 0 2px 6px rgba(76, 166, 255, 0.4), 0 10px 26px rgba(76, 166, 255, 0.48);
    }
}

:root[data-theme="dark"] {
    --color-background: var(--frost-950);
    --color-surface: var(--frost-800);
    --color-surface-muted: var(--frost-750);
    --color-surface-sunken: var(--frost-900);
    --color-text: var(--frost-50);
    --color-text-muted: var(--frost-400);
    --color-border: var(--frost-700);
    --color-border-strong: var(--frost-450);
    --color-accent: var(--blue-300);
    --color-accent-strong: var(--blue-200);
    --color-accent-contrast: var(--frost-1000);
    --color-accent-soft: var(--blue-900);
    --color-focus: var(--blue-200);
    --color-danger: var(--red-300);
    --color-warning: var(--amber-300);
    --color-success: var(--green-300);

    --glass-tint: rgba(37, 45, 57, 0.56);
    --glass-edge: rgba(255, 255, 255, 0.16);
    --glass-specular: rgba(255, 255, 255, 0.24);
    --glass-specular-low: rgba(255, 255, 255, 0.08);
    --glass-scrim-top: rgba(5, 8, 12, 0.28);
    --glass-scrim-bottom: rgba(5, 8, 12, 0.66);

    --elev-1: 0 1px 2px rgba(5, 8, 12, 0.4);
    --elev-2: 0 1px 3px rgba(5, 8, 12, 0.5), 0 10px 28px rgba(5, 8, 12, 0.6);
    --elev-3: 0 2px 6px rgba(76, 166, 255, 0.4), 0 10px 26px rgba(76, 166, 255, 0.48);
}

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

/* The `hidden` attribute wins over any `display` a rule below declares. The
   user agent's own `[hidden]` rule loses to an author `display`, so a hidden
   banner that also declared `display: block` would still be drawn. */
[hidden] {
    display: none !important;
}

html {
    -webkit-text-size-adjust: 100%;
}

/*
 * The reset (ADR-0068).
 *
 * Every native element the templates use, stripped of the user agent's own
 * chrome and of the margins this stylesheet's flow rules now own. It is not
 * a general-purpose reset: it names the tags `app/Views/**` actually contains,
 * and `StylesheetContractTest::testEveryNativeElementUsedIsReset` fails when a
 * template starts using one that is not here.
 *
 * `fieldset` is why it exists. On App Settings it rendered with the user
 * agent's `2px groove` border and its own padding — the one square box in an
 * application of rounded surfaces, and a sixteen-pixel indent nothing else had.
 */
fieldset,
legend,
figure,
blockquote,
form,
ul,
ol,
dl,
dd,
h1,
h2,
h3,
h4,
p {
    margin: 0;
    padding: 0;
}

fieldset {
    border: 0;
    /* A fieldset's `min-width: min-content` is a user-agent rule and the one
       remaining way a long token could widen the page (§25.8). */
    min-width: 0;
}

legend {
    display: block;
    width: 100%;
}

ul,
ol {
    list-style: none;
}

/* A disclosure is a control (ADR-0053): its marker is drawn by the stylesheet,
   not by the engine, so the two never appear together. */
details,
summary {
    list-style: none;
    cursor: pointer;
}

summary::-webkit-details-marker {
    display: none;
}

table {
    border-collapse: collapse;
}

button {
    font: inherit;
    color: inherit;
}

body {
    margin: 0;
    /* The baseline for a long word in normal flow. It is *not* enough for a
       flex or grid item: `break-word` does not reduce an element's min-content
       width, so an item still refuses to shrink below its longest token. Every
       item that carries generated text therefore declares `min-width: 0` and
       `overflow-wrap: anywhere` of its own (§25.8). */
    overflow-wrap: break-word;
    background: var(--color-background);
    color: var(--color-text);
    font-family: var(--font-stack);
    font-size: var(--text-md);
    line-height: var(--leading-normal);
}

h1,
h2,
h3 {
    line-height: var(--leading-tight);
}

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

:focus-visible {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

/*
 * Text that is in the document for assistive technology and out of the way for
 * everyone else — the calendar's day summary, which cannot be shown in a cell
 * two characters wide. Never used to hide a control (§25.8).
 */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    white-space: nowrap;
    clip-path: inset(50%);
}

/* Interaction states (ADR-0070) -------------------------------------------- */
/*
 * Five states for everything a user operates: rest, hover, pressed, focus and
 * disabled. Before this the stylesheet had one `:focus-visible` rule and six
 * `:active` rules and **no `:hover` and no `:disabled` at all** — so on a
 * desktop nothing in the application answered a pointer, which is the loudest
 * way an application says it is a document.
 *
 * Hover lives inside `@media (hover: hover)`, because a touch device that
 * emulates hover leaves the state stuck on the last thing tapped. The pressed
 * state is what a phone gets instead, and the platform's own grey tap flash is
 * suppressed so that the pressed state is *ours* — which is most of what
 * "native" means in the hand (§26.1).
 */

a,
button,
summary,
label,
select,
input,
textarea {
    -webkit-tap-highlight-color: transparent;
}

@media (hover: hover) {
    /* A row lifts onto the muted surface under the pointer. */
    .menu-list__link:hover,
    .record-list__link:hover,
    .trip-list__link:hover,
    .journey__link:hover,
    .calendar__link:hover,
    .panel__summary:hover {
        background: var(--color-surface-muted);
    }

    .button:hover {
        background: var(--color-accent-strong);
    }

    .button--secondary:hover {
        background: var(--color-surface-muted);
        border-color: var(--color-text-muted);
    }

    /*
     * A state has to differ from the thing it is a state *of*. Inside a muted
     * panel the surface below is already `--color-surface-muted`, so the hover
     * above resolved to the panel's own colour and nothing happened — on the
     * Review screen, on both master-data forms and on the vehicle form. It
     * takes one more step down there.
     *
     * The step is a measured surface rather than a translucent overlay, and
     * that is a decision rather than a shortcut (ADR-0075). A neutral wash over
     * whatever lies beneath is the usual answer and it cannot be used here:
     * this palette holds every text colour to 4.5:1 with two or three tenths of
     * headroom, and darkening the ground under the text spends exactly that.
     * Measured, a 5 % wash puts muted text on a hovered row at 4.40:1 and a 9 %
     * one at 4.04:1; in the dark the accent lands at 3.15:1. An opaque hover is
     * a colour the contrast check can see, and every one of them is in the
     * table it measures.
     */
    .panel--muted .button--secondary:hover {
        background: var(--color-surface-sunken);
    }

    .action-link:hover,
    .skip-link:hover {
        color: var(--color-accent-strong);
    }

    .view-switch__link:hover,
    .vehicle-switch__link:hover,
    .bottom-nav__link:hover {
        color: var(--color-text);
    }

    .bottom-nav__link--primary:hover .bottom-nav__icon {
        background: var(--color-accent-strong);
    }

    .field__input:hover {
        border-color: var(--color-text-muted);
    }

    .field__choice-label:hover {
        color: var(--color-text);
    }
}

/* Pressed: the surface goes one step down, the shape gives a little. */
.menu-list__link:active,
.record-list__link:active,
.trip-list__link:active,
.journey__link:active,
.calendar__link:active,
.panel__summary:active {
    background: var(--color-surface-sunken);
}

.action-link:active,
.field__choice-label:active {
    opacity: 0.7;
}

/*
 * Disabled: half the ink, no pointer, and no hover — a control that cannot be
 * used must not answer as though it can. ADR-0035 renders the energy
 * confirmation disabled until the browser has ticked the boxes for it, and
 * until now it looked exactly like an enabled control.
 */
:disabled,
[aria-disabled="true"] {
    opacity: 0.5;
    cursor: not-allowed;
}

.field__choice-label:has(:disabled) {
    opacity: 0.5;
    cursor: not-allowed;
}

/* Skip link ---------------------------------------------------------------- */

.skip-link {
    position: absolute;
    left: var(--space-md);
    top: -4rem;
    z-index: 20;
    display: flex;
    flex-direction: row;
    align-items: center;
    /* The first control on every page keeps the target of §25.8 too. */
    min-height: var(--touch-target);
    padding: var(--space-sm) var(--space-md);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-text);
    text-decoration: none;
}

.skip-link:focus {
    top: var(--space-md);
}

/* Flow (ADR-0068) ---------------------------------------------------------- */
/*
 * Three rules, and the whole application's vertical rhythm. A container puts
 * the space between its children; a child never carries it. The heading step is
 * tighter than the block step, so a label binds to what it introduces.
 */

.app-main > * + * {
    margin-top: var(--flow-loose);
}

.panel__body > * + *,
.panel--group > * + *,
.notice > * + *,
.form > * + * {
    margin-top: var(--flow);
}

.panel__body > :where(.panel__subtitle, .notice__title) + *,
.notice > .notice__title + * {
    margin-top: var(--flow-tight);
}

/* The group label sits on the page, and the surface follows it closely. */
.panel > .panel__title + * {
    margin-top: var(--flow-tight);
}

/*
 * A surface that is itself a layout — a flex row, a wrapping group — owns the
 * space between its own children through `gap`, so the flow rule steps back.
 * Two containers do this, and both say so here rather than each undoing the
 * rule where nobody would look for it.
 */
.vehicle-card > * + *,
.panel--compact > .panel__body > * + * {
    margin-top: 0;
}

/* Page shell --------------------------------------------------------------- */

.app-main {
    display: flex;
    flex-direction: column;
    /* Explicit, because on `.body--minimal` this is a flex item whose `auto`
       side margins switch off stretching: without a stated width the column
       would shrink-to-fit its widest line and take the page with it (§25.8). */
    width: 100%;
    max-width: var(--content-max);
    margin: 0 auto;
    padding: var(--space-lg) var(--page-pad);
    /* Room for the floating navigation plus the home indicator. */
    padding-bottom: calc(var(--nav-height) + var(--space-xl) + env(safe-area-inset-bottom, 0px));
}

.app-main:focus {
    outline: none;
}

/*
 * A page rendered while the application state is unknown or irrelevant: the
 * sign-in screen, the error pages, the migration gate, the offline notice. It
 * carries no navigation, so its content is centred in the viewport rather than
 * pushed against the top, and its column is the width of a sign-in form rather
 * than the width of a logbook.
 *
 * `flex-direction` is stated, as it is in every flex rule in this file. The
 * default is `row`, and this rule was once written for a page that passes one
 * child and then inherited by one that passes three — which put the heading,
 * the notice and the form beside each other (ADR-0063).
 */
.body--minimal {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    min-height: 100dvh;
}

.app-main--minimal {
    flex: 1;
    justify-content: center;
    max-width: calc(var(--measure-control) + 2 * var(--space-md));
    padding-bottom: var(--space-xl);
}

/* The installer is a form flow, so it starts at the top of the page. */
.app-main--install {
    padding-bottom: var(--space-xl);
}

.page-header {
    min-width: 0;
}

.page-header__title {
    font-size: var(--text-2xl);
    font-weight: var(--weight-title);
    letter-spacing: var(--track-2xl);
}

.page-header__subtitle {
    max-width: var(--measure);
    margin: var(--space-xs) 0 0;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

/* Figures (§26.1: large, clear numeric values) ------------------------------ */
/* The numbers a driver opens the app for. The label sits above the value,
   because a label beside a five-digit odometer reading does not fit on a narrow
   phone; the values themselves keep their size and never wrap. */

.figures {
    display: grid;
    /* One column at the narrowest width: two columns of 112 pixels would break
       `48.400 km` across two lines, and a headline figure that wraps is not a
       headline figure (§26.1). */
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-md);
}

@media (min-width: 22rem) {
    .figures {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

.figure {
    min-width: 0;
}

.figure__label {
    display: block;
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    letter-spacing: var(--track-xs);
}

.figure__value {
    margin: 0;
    font-size: var(--text-xl);
    font-weight: var(--weight-title);
    line-height: var(--leading-tight);
    letter-spacing: var(--track-xl);
    /* Digits of equal width, so the four figures line up (§26.1). */
    font-variant-numeric: tabular-nums;
    overflow-wrap: anywhere;
}

@media (min-width: 30rem) {
    .figure__value {
        font-size: var(--text-figure);
        letter-spacing: var(--track-figure);
    }
}

/* The fast vehicle switcher (§25.2) ----------------------------------------- */
/* One control shape with the Logbook's view switch below: a track of pills that
   scrolls sideways inside itself rather than wrapping, so a long vehicle name
   cannot turn the switcher into a two-line band above every page. The track is
   `overflow-x: auto` and is therefore not page-level scrolling (§25.8). */

.vehicle-switch {
    min-width: 0;
}

.vehicle-switch__list {
    display: flex;
    flex-direction: row;
    gap: var(--space-xs);
    padding: var(--space-xs);
    border-radius: var(--radius-pill);
    background: var(--color-surface-sunken);
    overflow-x: auto;
    scrollbar-width: none;
}

.vehicle-switch__list::-webkit-scrollbar {
    display: none;
}

.vehicle-switch__item {
    flex: none;
    min-width: 0;
}

.vehicle-switch__link {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--space-xs);
    min-height: var(--touch-target);
    padding: var(--space-xs) var(--space-md);
    border-radius: var(--radius-pill);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    white-space: nowrap;
    text-decoration: none;
    transition: background-color var(--duration-fast) var(--ease-standard);
}

/* The vehicle on screen is named by a word as well as by weight and fill, so
   the state never depends on colour alone (§25.8, §26.1). */
.vehicle-switch__link.is-active {
    background: var(--color-surface);
    box-shadow: var(--elev-1);
    color: var(--color-text);
    font-weight: var(--weight-label);
}

.vehicle-switch__state {
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-body);
}

/* Panels ------------------------------------------------------------------- */
/*
 * A panel is a **group**, not a card: a quiet label on the page background and,
 * below it, one surface carrying the content (ADR-0069). That is the grouped
 * list every platform's settings screen uses, and it is what stops a page of
 * panels reading as a stack of titled boxes — the content leads and the label
 * marks where a section starts.
 *
 * `.panel` therefore has no fill, no border and no padding of its own. The
 * surface is `.panel__body`, and every panel that has a title has exactly one.
 */

.panel {
    /* The group. Its own spacing comes from the flow rules above. */
    min-width: 0;
}

.panel__body {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--elev-0);
    padding: var(--surface-pad);
}

.panel--muted > .panel__body,
.panel--muted.panel__body {
    background: var(--color-surface-muted);
}

/* A group with nothing in it says so on one line rather than in a whole card. */
.panel--compact > .panel__body {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-xs) var(--space-sm);
    padding: var(--space-sm) var(--surface-pad);
}

.panel__text--quiet {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

/*
 * A group that carries something the user has to decide about — the documented
 * odometer exception of §13.2. The border does the work rather than a colour
 * fill, so the meaning survives a forced-colours mode (§25.8).
 */
.panel--warning > .panel__body {
    border-left-width: 4px;
    border-left-color: var(--color-danger);
}

/*
 * The group label. Small, quiet and letterspaced: it says where a section
 * starts and then gets out of the way, which is the opposite of the loud
 * heading M15 introduced and the reason a page now reads as one grouped page
 * rather than as a stack of cards with titles (ADR-0069).
 */
.panel__title {
    padding: 0 var(--surface-pad);
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-label);
    letter-spacing: var(--track-xs);
}

/*
 * A panel that belongs to one trip category (§25.4). It is a `<details>`, so the
 * disclosure is native: operable by keyboard, announced as expanded or
 * collapsed, and working with no JavaScript at all. The summary keeps the 44px
 * target of §25.8 and its own marker, so the state is never carried by colour.
 *
 * It is the one panel that stays a single surface with a header inside it: the
 * summary is both the label and the control, and a control the user taps has to
 * be one surface.
 */

.panel--group {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--surface-pad);
}

/* The summary bleeds to the surface's edges so the whole header row is the
   target, and the negative inset is exactly the surface padding — one value,
   not two guesses (§25.8). */
.panel__summary {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--space-sm);
    min-height: var(--touch-target);
    margin: calc(-1 * var(--surface-pad)) calc(-1 * var(--surface-pad)) 0;
    padding: var(--space-xs) var(--surface-pad);
    cursor: pointer;
    list-style: none;
}

/* Safari draws its own triangle through this pseudo-element. */
.panel__summary::-webkit-details-marker {
    display: none;
}

/* The same chevron every row uses, pointing down when the group is open. A text
   glyph at the right edge was the affordance for opening a whole field group;
   this one is drawn, sized and animated like the rest of the system. */
.panel__summary::after {
    content: "";
    flex: none;
    width: 0.5rem;
    height: 0.5rem;
    margin-left: auto;
    margin-right: 0.25rem;
    border-top: 2px solid var(--color-text-muted);
    border-right: 2px solid var(--color-text-muted);
    transform: rotate(45deg);
    transition: transform var(--duration-base) var(--ease-standard);
}

.panel--group[open] > .panel__summary::after {
    transform: rotate(135deg);
}

/* Inside a disclosure the summary *is* the header, so the title reads as one:
   the group-label treatment would make a tappable row look like a caption. */
.panel__summary .panel__title {
    flex: 1;
    min-width: 0;
    padding: 0;
    color: var(--color-text);
    font-size: var(--text-md);
    font-weight: var(--weight-label);
    /* Body size is the hinge of the tracking scale and takes none. */
    letter-spacing: normal;
}

/* The flow rule puts the gap under an open summary; a closed one has no
   following sibling to be separated from, so the surface closes up. */
.panel--group:not([open]) {
    padding-bottom: 0;
}

.panel__text {
    max-width: var(--measure);
}

/* A heading inside a surface — the category distribution below Overview's
   figures, the group of a data-quality check (§25.2, §33). One level below the
   group label and inside the card, so it is the loud one of the pair. */
.panel__subtitle {
    color: var(--color-text);
    font-size: var(--text-md);
    font-weight: var(--weight-label);
}

/* Buttons ------------------------------------------------------------------ */

.button {
    display: inline-flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: var(--space-xs);
    min-height: var(--touch-target);
    min-width: var(--touch-target);
    padding: var(--space-sm) var(--space-lg);
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    background: var(--color-accent);
    color: var(--color-accent-contrast);
    font-size: var(--text-md);
    font-weight: var(--weight-label);
    text-decoration: none;
    transition: transform var(--duration-fast) var(--ease-standard);
}

.button:active {
    transform: scale(0.98);
}

.button--secondary {
    background: var(--color-surface);
    border-color: var(--color-border-strong);
    color: var(--color-text);
}

/* Forms -------------------------------------------------------------------- */
/* One row shape everywhere: label, control, hint, error, in that order. The
   label is the loudest part of the row and the hint the quietest, so a form
   scans as a list of questions rather than as alternating paragraphs. */

.form {
    /* The container of one form's fields. Its own spacing comes from the flow
       rules; a `margin: 0` here would out-specify them, which is how a form
       came to sit flush against the header above it. */
    min-width: 0;
}

/* A form that is one control inside a box — the Dismiss button of a help box
   (§25.7) — rather than a section of the page. */
.form--inline {
    display: inline-block;
}

.form--inline .form__actions {
}

.field {
    min-width: 0;
}

.field__label {
    display: block;
    margin-bottom: var(--space-xs);
    font-size: var(--text-md);
    font-weight: var(--weight-label);
}

.field__input {
    display: block;
    width: 100%;
    min-height: var(--touch-target);
    padding: var(--space-sm) var(--space-md);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-text);
    font-family: inherit;
    font-size: var(--text-md);
}

/*
 * iOS draws a date or time control at its own width, with its value centred,
 * whatever the rule above says: on the owner's iPhone the three of them ran
 * past the panel while the text inputs beside them fitted (1.1.1). Without
 * the platform appearance they take the width the rule gives them and start
 * their value where every other field does. The height is the touch target
 * already, so an empty time does not collapse. The search field is in the
 * same list for the same reason: iOS gives it a rounded inset look of its
 * own that ignores the border and the radius above.
 */
.field__input[type="date"],
.field__input[type="time"],
.field__input[type="search"] {
    -webkit-appearance: none;
    appearance: none;
    min-width: 0;
    text-align: start;
}

.field__input::-webkit-date-and-time-value {
    text-align: start;
}

.field__input::-webkit-search-decoration {
    -webkit-appearance: none;
}

.field__hint {
    display: block;
    max-width: var(--measure);
    margin-top: var(--space-xs);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

/* The container is always in the document, so the field's `aria-describedby`
   always names something (§25.8); with nothing in it, it takes no space. */
.field__error:empty {
    display: none;
}

/*
 * A field that failed validation marks its *control*, not only the line of text
 * under it. The message was correct and associated and specific, and on a form
 * five thousand pixels tall the user still had to hunt for it, because the
 * input looked exactly like a valid one. `:has()` means no template has to
 * remember to add a class; where it is unsupported the message alone still
 * shows, which is the behaviour that was there before.
 */
.field:has(.field__error-line) .field__input {
    border-color: var(--color-danger);
    border-width: 2px;
}

.field__error-line {
    display: block;
    margin-top: var(--space-xs);
    color: var(--color-danger);
    font-weight: var(--weight-label);
    font-size: var(--text-sm);
}

/* Checkbox rows keep the 44px target on the control *and* on its label, so the
   whole row is comfortable to hit on a phone (§25.8). */

.field--choice {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--space-sm);
}

.field__checkbox {
    flex: none;
    width: 1.5rem;
    height: 1.5rem;
    margin: 0;
    accent-color: var(--color-accent);
}

/*
 * The box sits inside its label, and the gap belongs here rather than on the
 * row: a screen that renders the label without `.field--choice` around it — App
 * Settings does — otherwise puts the box flush against its own text.
 */
.field__choice-label {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--space-sm);
    min-height: var(--touch-target);
}

.field__optional {
    margin-left: var(--space-xs);
    color: var(--color-text-muted);
    font-weight: var(--weight-body);
    font-size: var(--text-sm);
}

/* A group of radio options — trip category, journey choice. Each row keeps the
   44px target and its explanation sits under the label rather than beside it, so
   nothing has to shrink at 320px (§25.8). */

.field--options {
    display: block;
    padding: 0;
    border: 0;
}

.field--options .field__choice-label {
    align-items: flex-start;
    padding: var(--space-xs) 0;
}

.field--options .field__checkbox {
    margin-top: 0.15rem;
}

.field--options .field__hint {
    margin-top: 0;
}

textarea.field__input {
    min-height: calc(var(--touch-target) * 2);
    resize: vertical;
}

/*
 * An action row has one shape per viewport (ADR-0068). Below 30rem every action
 * is the full width and they stack, primary first; from 30rem they sit in a row
 * at their content width. A wrapping row of content-sized buttons produced six
 * different widths and three row shapes on one phone screen.
 */
.form__actions {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-sm);
}

@media (min-width: 30rem) {
    .form__actions {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
    }
}

/* Menus (More) ------------------------------------------------------------- */

.menu-list {
    min-width: 0;
}

.menu-list__item {
    border-bottom: 1px solid var(--color-border);
}

.menu-list__item:last-child {
    border-bottom: 0;
}

/*
 * The whole row is the target (§25.8). `overflow-wrap: anywhere` is not
 * decoration here: the text inside a flex container is an anonymous flex item
 * whose automatic minimum is its longest token, so a generated file name like
 * `tournal_vehicle_1_2026_master_data.tsv` pushed the export result screen 35
 * pixels past a 320-pixel viewport. `break-word` — the baseline on `body` —
 * does not reduce that minimum; `anywhere` does.
 */
.menu-list__link {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--space-md);
    min-width: 0;
    min-height: var(--touch-target);
    color: var(--color-text);
    font-weight: var(--weight-label);
    overflow-wrap: anywhere;
    text-decoration: none;
}

.menu-list__label {
    flex: 1;
    min-width: 0;
}

/* Vehicles ----------------------------------------------------------------- */
/* The image is decorative: it never carries information the text lacks, and the
   neutral fallback is drawn inline so a host without pictures still looks
   finished (§10.4, §26.1). */

.vehicle-image {
    display: flex;
    flex-direction: row;
    flex: none;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    border-radius: var(--radius-md);
    background: var(--color-surface-muted);
    color: var(--color-text-muted);
    object-fit: cover;
}

/* The drawn fallback gets a border so it reads as a placeholder rather than as
   a picture that failed to load (§10.4). */
.vehicle-image--neutral {
    border: 1px solid var(--color-border);
}

.vehicle-image--small {
    width: 4rem;
    height: 3rem;
}

.vehicle-image--large {
    width: 100%;
    height: 8rem;
}

.vehicle-image svg {
    width: 70%;
    height: auto;
}

/*
 * §26.1 asks for vehicle presentation, not a thumbnail beside a caption. The
 * picture is the width of the card and the name sits under it; the label
 * "Active vehicle" is what it always was — the quiet part.
 */
.vehicle-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
}

.vehicle-card__body {
    min-width: 0;
}

.vehicle-card__label {
    margin: 0;
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-label);
    letter-spacing: var(--track-xs);
}

.vehicle-card__name {
    margin: var(--space-xs) 0;
    font-size: var(--text-xl);
    font-weight: var(--weight-title);
    letter-spacing: var(--track-xl);
}

@media (min-width: 30rem) {
    .vehicle-card {
        flex-direction: row;
        align-items: center;
    }

    .vehicle-image--large {
        width: 11rem;
        flex: none;
    }
}

.vehicle-list {
    min-width: 0;
}

.vehicle-list__item {
    display: flex;
    flex-direction: row;
    gap: var(--space-md);
    padding: var(--space-md) 0;
    border-bottom: 1px solid var(--color-border);
}

.vehicle-list__item:last-child {
    border-bottom: 0;
}

.vehicle-list__body {
    /* Without this a long licence plate would widen the row and reintroduce
       page-level horizontal scrolling at 320px (§25.8). */
    min-width: 0;
    flex: 1;
}

.vehicle-list__name {
    margin: 0;
    font-size: var(--text-lg);
    letter-spacing: var(--track-lg);
}

.vehicle-list__facts {
    margin: 0 0 var(--space-sm);
    color: var(--color-text-muted);
}

/*
 * Label above value at the narrowest width, two columns from 30rem. A track
 * whose minimum is 8rem cannot shrink, and inside a row that also carries the
 * vehicle picture it pushed the page 16 pixels past a 320-pixel viewport; both
 * tracks now state a minimum of zero, so the grid can always fit what it is
 * given (§25.8).
 */
.vehicle-list__data {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
    margin: 0 0 var(--space-sm);
}

.vehicle-list__data dd + dt {
    margin-top: var(--flow);
}

.vehicle-list__data dt {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

.vehicle-list__data dd {
    margin: 0;
    overflow-wrap: anywhere;
}

@media (min-width: 30rem) {
    .vehicle-list__data {
        grid-template-columns: minmax(0, 9rem) minmax(0, 1fr);
        gap: var(--space-xs) var(--space-sm);
    }

    .vehicle-list__data dd + dt {
        margin-top: 0;
    }

    .vehicle-list__data dt {
        font-size: var(--text-md);
    }
}

.vehicle-list__actions {
    margin: 0;
}

/* Master-data lists (§16, §17, §20.2) -------------------------------------- */

.record-list {
    min-width: 0;
}

.record-list__item {
    /* min-width keeps a long address from widening the row and reintroducing
       page-level horizontal scrolling at 320px (§25.8). */
    min-width: 0;
    padding: var(--space-md) 0;
    border-bottom: 1px solid var(--color-border);
}

.record-list__item:last-child {
    border-bottom: 0;
}

/*
 * A row that opens a record: the whole row is the link, it carries the
 * 44-pixel target of §25.8, and the chevron says where it goes. A record whose
 * row has more than one action keeps its buttons instead; a row with exactly
 * one is this.
 */
.record-list__link {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--space-md);
    min-width: 0;
    min-height: var(--touch-target);
    color: inherit;
    text-decoration: none;
}

.record-list__body {
    display: block;
    flex: 1;
    min-width: 0;
}

/* A row's name. A record's name is short; a trip's is a whole formatted
   address, so the title is clamped rather than allowed to run to four lines
   (§16.1 keeps the snapshot; the row shows what fits). */
.record-list__title {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
    margin: 0;
    font-size: var(--text-lg);
    letter-spacing: var(--track-lg);
    font-weight: var(--weight-label);
    overflow-wrap: anywhere;
}

/* What a row is about before it says what it is: a status tag and a date. */
.record-list__meta {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--space-sm);
    margin: 0 0 var(--space-xs);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
}

.record-list__link .record-list__subtitle,
.record-list__link .record-list__title {
    margin-bottom: 0;
}

.record-list__subtitle {
    display: block;
    margin: 0 0 var(--space-sm);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    overflow-wrap: anywhere;
}

/*
 * The one "this row goes somewhere" mark. Drawn from borders rather than from a
 * glyph, so it needs no icon font and no character that a system font might
 * render as an emoji; `aria-hidden`, because the row's own text is its name.
 */
.chevron {
    flex: none;
    width: 0.5rem;
    height: 0.5rem;
    border-top: 2px solid var(--color-text-muted);
    border-right: 2px solid var(--color-text-muted);
    transform: rotate(45deg);
}

/* A secondary line inside a record row — an allocation's reason, a history
   entry's actor. Not a status: that is `.tag`. */
.record-list__flag {
    margin: 0 0 var(--space-sm);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

.record-list__tags {
    display: block;
    margin-top: var(--space-xs);
}

.record-list__actions {
    margin: 0;
}

/* A text action at the end of a row: the weight of a link, the target of a
   button, and none of a button's furniture (§25.8). */
.action-link {
    display: inline-flex;
    flex-direction: row;
    align-items: center;
    gap: var(--space-sm);
    min-height: var(--touch-target);
    color: var(--color-accent);
    font-weight: var(--weight-label);
    text-decoration: none;
}

.action-link .chevron {
    border-color: currentColor;
}

/* Status tags (ADR-0065) ---------------------------------------------------- */
/*
 * The one way this application says what state a record is in: a word in a
 * shape. Review's severities, a trip's flags, an installer check, an archived
 * or withdrawn record — all of them were saying it differently, and one of them
 * (an export marked DRAFT inside a green box) was saying two things at once.
 *
 * The word is always present, so the colour is never the only carrier (§25.8),
 * and every variant's colour is measured against every surface a tag sits on.
 */

.tag {
    display: inline-flex;
    flex-direction: row;
    align-items: center;
    margin-right: var(--space-xs);
    padding: 0 var(--space-sm);
    border: 1px solid currentColor;
    border-radius: var(--radius-sm);
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-label);
    letter-spacing: var(--track-xs);
    white-space: nowrap;
}

.tag--neutral {
    color: var(--color-text-muted);
}

.tag--success {
    color: var(--color-success);
}

.tag--warning {
    color: var(--color-warning);
}

.tag--danger {
    color: var(--color-danger);
}

/* Notices ------------------------------------------------------------------ */

/* A notice is a surface, so its text starts on the same edge as a panel's and
   its corner is the same corner (ADR-0069). The status edge is three pixels
   thicker than a panel's hairline, and the padding gives those three back, so
   the text lands where every other surface puts it. */
.notice {
    padding: var(--surface-pad);
    padding-left: calc(var(--surface-pad) - 3px);
    border: 1px solid var(--color-border);
    border-left-width: 4px;
    border-radius: var(--radius-lg);
    background: var(--color-surface);
}

.notice--error {
    border-left-color: var(--color-danger);
}

.notice--success {
    border-left-color: var(--color-success);
}

/* A send that could not be made, or could not be confirmed (ADR-0077). Not an
   error — nothing is wrong with what was entered — and not a success. */
.notice--warning {
    border-left-color: var(--color-warning);
}

/*
 * A dismissible explanation (§25.7). It is not a status, so it does not borrow
 * a status colour: the accent edge says "read this once", and the Dismiss
 * button says the rest.
 */
.notice--help {
    border-left-color: var(--color-accent);
    background: var(--color-surface);
}

.notice--help .panel__text,
.notice--help p {
    max-width: var(--measure);
}

/* Its title, and its Dismiss button close to the text it belongs to rather than
   a whole form gap below it. */
.notice__title {
    font-weight: var(--weight-label);
}

/* Check lists (installer requirements, self-check) -------------------------- */

.checklist {
    min-width: 0;
}

.checklist__item {
    display: flex;
    flex-direction: row;
    /* A tag is a label, not a column: without this the flex row stretches it to
       the row's full height and it reads as a button (ADR-0065). */
    align-items: flex-start;
    gap: var(--space-sm);
    padding: var(--space-sm) 0;
    border-bottom: 1px solid var(--color-border);
}

.checklist__item:last-child {
    border-bottom: 0;
}

/* The status word sits in its own column, so a list of checks reads down the
   left edge rather than at the start of each sentence. It keeps a tag's own
   height; the column is the width. */
.checklist__status {
    flex: 0 0 4.5rem;
    justify-content: center;
}

/* The sentence beside the status word. It takes the rest of the row and may
   shrink below its longest token, which is what an unbreakable identifier in a
   check message needs (§25.8). */
.checklist__text {
    flex: 1;
    min-width: 0;
    overflow-wrap: anywhere;
}

/* The rows behind a data-quality finding (§33). Indented rather than boxed, so
   a long list of identifiers still wraps inside 320 CSS pixels (§25.8). */
.checklist__details {
    margin: var(--space-xs) 0 0;
    padding-left: var(--space-md);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    overflow-wrap: anywhere;
}

/* Installation steps ------------------------------------------------------- */

/*
 * Where the installation has got to. Not a control — the steps are not links —
 * but the same track the segmented switchers use, because "one of these is the
 * current one" is the same idea and deserves the same shape. It scrolls rather
 * than wrapping: five step names wrapped to two ragged lines at 390.
 */
.steps {
    display: flex;
    flex-direction: row;
    gap: var(--space-xs);
    padding: var(--space-xs);
    border-radius: var(--radius-pill);
    background: var(--color-surface-sunken);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    overflow-x: auto;
    scrollbar-width: none;
}

.steps::-webkit-scrollbar {
    display: none;
}

.steps__item {
    flex: none;
    padding: var(--space-xs) var(--space-md);
    border-radius: var(--radius-pill);
    white-space: nowrap;
}

/* The current step is lifted out of the track, as a chosen segment is; the word
   and the weight say which it is, so the fill is never the only carrier. */
.steps__item.is-current {
    background: var(--color-surface);
    box-shadow: var(--elev-1);
    color: var(--color-text);
    font-weight: var(--weight-label);
}

.steps__item.is-done::before {
    content: "✓ ";
}

/*
 * Continuous corners, where the browser has them (ADR-0074).
 *
 * A circular corner meets its straight edge at a point where the curvature
 * jumps from zero to its full value, and the eye reads that discontinuity even
 * though it cannot name it. A superellipse ramps the curvature instead, which
 * is what every native platform's rounded rectangle actually is.
 *
 * Purely additive: without `corner-shape` the same radii draw the same circular
 * corners they draw today, so there is nothing to fall back to and nothing to
 * test beyond the guard that the block is here. Pills are left alone — a
 * continuous corner on a shape that is all corner has nothing to ramp into —
 * which since the navigation became a capsule includes the bar and its items.
 */
@supports (corner-shape: squircle) {
    .panel__body,
    .panel--group,
    .notice,
    .button,
    .field__input,
    .vehicle-image,
    .error-page {
        corner-shape: squircle;
    }
}

/* Connection --------------------------------------------------------------- */

/*
 * The one banner in the application, shown only when the browser is certain
 * it is offline (ADR-0077). It sits before <main> in the layout, outside every
 * flow rule, and sticks to the top so the fact stays in view while the user
 * scrolls a form they are about to fail to send. Its colours are the notice's —
 * text on a surface, both measured — with the warning colour as a bottom edge
 * rather than a left one, because a top banner is read across, not down.
 */
.connection {
    position: sticky;
    top: 0;
    /* Below the floating navigation, above everything that scrolls. */
    z-index: 9;
    padding: var(--space-sm) var(--page-pad);
    padding-top: calc(var(--space-sm) + env(safe-area-inset-top, 0px));
    border-bottom: 3px solid var(--color-warning);
    background: var(--color-surface);
    box-shadow: var(--elev-1);
}

.connection__text {
    max-width: var(--measure);
    margin-inline: auto;
    color: var(--color-text);
    font-size: var(--text-sm);
}

/* Error pages -------------------------------------------------------------- */

.error-page {
    width: 100%;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--surface-pad);
}

.error-page > * + * {
    margin-top: var(--flow);
}

.error-page__status {
    margin: 0 0 var(--space-xs);
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
    font-size: var(--text-sm);
    letter-spacing: var(--track-sm);
}

.error-page__title {
    font-size: var(--text-xl);
    letter-spacing: var(--track-xl);
    font-weight: var(--weight-title);
}

.error-page__text {
    max-width: var(--measure);
    color: var(--color-text);
}

.error-page__reference {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

/* Bottom navigation (§25.2, §26.1) ------------------------------------------ */

.bottom-nav {
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    /*
     * A capsule, placed concentrically with the display's own corners
     * (ADR-0074 §2, applied at the outer boundary). The bar measures 58px
     * tall, so its capsule radius is 29; the display corner of a current
     * iPhone is about 55pt; so a concentric bar sits 55 − 29 = 26 from the
     * screen edge, and its curve runs parallel to the glass. Sideways it sits
     * `--space-md` + `--space-xs` = 20. At the bottom the distance is measured
     * from the physical edge, not from the home indicator's inset: the inset
     * (34 on those phones) minus 12 = 22, which leaves the indicator itself
     * clear below the bar, where Apple's own tab bar leaves it. Both within a
     * few pixels of concentric, and both from tokens. Without an inset — a
     * desktop, a browser with its own toolbar — the bar keeps the 8 it
     * always had.
     */
    bottom: max(var(--space-sm), env(safe-area-inset-bottom, 0px) - var(--space-sm) - var(--space-xs));
    width: calc(100% - 2 * (var(--space-md) + var(--space-xs)));
    max-width: 30rem;
    z-index: 10;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    /* Opaque by default; the translucent treatment is layered on only where it
       is supported and not opted out of, so the fallback is never the broken
       case (§25.8, §26.1). */
    background: var(--color-surface);
    /* Restrained, and lifted rather than dropped: the bar floats above the page
       instead of sitting on it (§26.1). Two shadows, because one soft one is
       invisible against this page (ADR-0071). */
    box-shadow: var(--elev-2);
}

@supports (backdrop-filter: blur(18px)) or (-webkit-backdrop-filter: blur(18px)) {
    .bottom-nav {
        /*
         * Five layers, and the surface needs all of them to read as a material
         * rather than as a blurred rectangle (ADR-0074):
         *
         * 1. the backdrop — blur *and* saturate. The saturation is not
         *    decoration: without it the colours passing underneath desaturate
         *    into grey and the bar looks like frosted bathroom glass;
         * 2. the tint, translucent enough to show movement underneath and
         *    opaque enough to keep the labels at full contrast against any
         *    content (§26.1: adaptive contrast);
         * 3. the edge, a hairline of light around the whole shape;
         * 4. the specular highlight along the top inside edge, with a weaker
         *    reflex along the bottom. This is the one that carries perceived
         *    thickness, and it is the layer that was missing;
         * 5. the content, fully opaque, which the rules below draw.
         *
         * The drop shadow stays underneath all five and is unchanged.
         */
        background: var(--glass-tint);
        border-color: var(--glass-edge);
        -webkit-backdrop-filter: blur(24px) saturate(190%);
        backdrop-filter: blur(24px) saturate(190%);
        box-shadow:
            inset 0 1px 0 0 var(--glass-specular),
            inset 0 -1px 0 0 var(--glass-specular-low),
            var(--elev-2);
    }

    /*
     * A scrim between the page and the bar. Content scrolling under a
     * translucent surface is the one place where the blur alone is not enough,
     * and it is drawn on a pseudo-element so nothing about it can catch a
     * pointer or a focus ring.
     */
    .bottom-nav::before {
        content: "";
        position: absolute;
        inset: -1px;
        z-index: -1;
        border-radius: inherit;
        background: linear-gradient(to bottom, var(--glass-scrim-top), var(--glass-scrim-bottom));
        pointer-events: none;
    }
}

@media (prefers-reduced-transparency: reduce) {
    .bottom-nav {
        background: var(--color-surface);
        border-color: var(--color-border);
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
    }

    .bottom-nav::before {
        display: none;
    }
}

.bottom-nav__list {
    display: flex;
    flex-direction: row;
    padding: var(--space-xs);
}

.bottom-nav__item {
    flex: 1 1 0;
    min-width: 0;
}

.bottom-nav__link {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    min-height: var(--touch-target);
    padding: var(--space-xs) 2px;
    /*
     * A pill inside a pill (ADR-0074 §2). While the bar was a rounded
     * rectangle this was the one place the concentric rule had anything to
     * say — the container's radius minus the list's inset, 14 against a
     * picked 12. The bar is a capsule now, and a pill inside a pill is
     * concentric at any distance, like the view switcher, the vehicle switcher
     * and the installer's steps always were. The rule stays recorded for the
     * next rounded rectangle inside another.
     */
    border-radius: var(--radius-pill);
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    text-decoration: none;
    transition:
        color var(--duration-base) var(--ease-standard),
        background-color var(--duration-base) var(--ease-standard),
        transform var(--duration-fast) var(--ease-standard);
}

/* The icon is drawn inline and marked `aria-hidden`; the label carries the
   accessible name. It sits on its own line above the label and never grows
   with the text size, so five destinations keep the same rhythm whatever the
   language does to their labels (§7.2). */
.bottom-nav__icon {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    height: 1.5rem;
}

/* §7.2: "layouts MUST tolerate longer German strings". A label wraps rather
   than truncating — "Fahrtenbuch" cut to "Fahrten…" names a different thing —
   and the bar grows by one line, which the page's bottom padding already has
   room for. This was the one truncation point in a stylesheet that wraps
   everywhere else. */
.bottom-nav__label {
    max-width: 100%;
    text-align: center;
    line-height: 1.15;
    overflow-wrap: anywhere;
    hyphens: auto;
}

/* The active destination is marked by weight, by a filled shape and by an
   underline as well as by colour: status must never be conveyed by colour alone
   (§26.1, §25.8). */
.bottom-nav__link.is-active {
    background: var(--color-accent-soft);
    color: var(--color-accent);
    font-weight: var(--weight-label);
}

.bottom-nav__link.is-active .bottom-nav__label {
    text-decoration: underline;
    /* Placed explicitly rather than by the font: a 12px label in a pill with
       four pixels under it has no room for a platform's idea of "auto". At
       one pixel, two below the baseline, the line ends where the label's own
       box does and stays inside the pill on iOS, where the old three pixels
       at auto thickness reached past the edge (owner's report, 1.1.3). */
    text-decoration-thickness: 1px;
    text-underline-offset: 2px;
}

.bottom-nav__link:active {
    transform: scale(0.94);
}

/*
 * New Trip is the prominent central action (§25.2), and §26.1 asks for it to be
 * "slightly elevated". What raises is the **icon**, not the whole item: raising
 * the item took its label 6 pixels off the baseline the other four share while
 * the pill cleared the bar's top edge by one, so the eye read a misalignment
 * rather than an elevation (ADR-0071).
 *
 * The circle is 2.75rem tall and gives 1.25rem of that back as a negative top
 * margin, so it occupies exactly the 1.5rem of flow an ordinary icon does — all
 * five labels stay on one baseline — and rises twelve pixels clear of the bar,
 * where a shadow of its own colour can be seen.
 */
.bottom-nav__link--primary {
    color: var(--color-text-muted);
}

.bottom-nav__link--primary .bottom-nav__icon {
    width: 2.75rem;
    height: 2.75rem;
    margin-top: -1.25rem;
    border-radius: var(--radius-pill);
    background: var(--color-accent);
    color: var(--color-accent-contrast);
    /* The top of the elevation ramp — its glow is the accent's own colour —
       and the same specular pair the bar carries, so the raised circle reads
       as the same material and not as a flat disc on top of it (ADR-0074). */
    box-shadow:
        var(--elev-3),
        inset 0 1px 0 0 var(--glass-specular),
        inset 0 -1px 0 0 var(--glass-specular-low);
    transition: background-color var(--duration-fast) var(--ease-standard);
}

.bottom-nav__link--primary.is-active {
    background: none;
    color: var(--color-text);
}

.bottom-nav__link--primary:active .bottom-nav__icon {
    transform: scale(0.92);
}

@media (prefers-reduced-motion: reduce) {
    /* The central action keeps its place, its fill and its elevation; only the
       movement goes. Nothing here removes a control or a piece of information
       (§25.8). */
    .bottom-nav__link--primary:active .bottom-nav__icon,
    .bottom-nav__link:active,
    .button:active {
        transform: none;
    }

    * {
        animation-duration: 0.001ms !important;
        transition-duration: 0.001ms !important;
    }
}

@media (min-width: 40rem) {
    .bottom-nav__link {
        font-size: var(--text-xs);
    }
}

/* Trips and journeys (§25.2) ----------------------------------------------- */
/* The whole row is the link, so the touch target is the row rather than the
   text inside it (§25.8). Long place names wrap instead of pushing the page
   sideways at 320 CSS pixels. */

/*
 * The same shape: one column of label-above-value at the narrowest width, two
 * from 30rem, with a fixed label measure from 48rem so a two-word label stops
 * wrapping. Both tracks state a minimum of zero, so neither can force the page
 * sideways (§25.8).
 */
/* Stacked, a pair has to read as a pair: the value sits close under its label
   and the next label starts a step away. One gap for both would make a list of
   pairs read as a list of lines. */
.data-list {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
}

.data-list dd + dt {
    margin-top: var(--flow);
}

.data-list dt {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

@media (min-width: 30rem) {
    .data-list {
        grid-template-columns: minmax(0, 10rem) minmax(0, 1fr);
        gap: var(--space-xs) var(--space-md);
    }

    .data-list dd + dt {
        margin-top: 0;
    }

    .data-list dt {
        font-size: var(--text-md);
    }
}

.data-list dd {
    margin: 0;
    overflow-wrap: anywhere;
}

/* One journey and the trips inside it, in the Logbook's default view (§25.2). */
.journey {
    min-width: 0;
}

.journey__title {
    margin: 0;
}

/* The heading row opens the journey. It is the row that is the target, not the
   words in it: a one-line title measured 19.5 pixels tall before this (§25.8). */
.journey__link {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--space-md);
    min-width: 0;
    min-height: var(--touch-target);
    color: inherit;
    text-decoration: none;
}

.journey__name {
    flex: 1;
    min-width: 0;
    overflow-wrap: anywhere;
}

.journey__facts {
    margin: var(--space-xs) 0 var(--space-md);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

/* Exports (§27, §28) -------------------------------------------------------- */
/* A checksum is 64 characters of hex that must stay readable and must never push
   the page sideways at 320 CSS pixels (§25.8). */

.checksum-value {
    display: block;
    font-family: ui-monospace, "SFMono-Regular", "Menlo", "Consolas", monospace;
    font-size: var(--text-xs);
    color: var(--color-text-muted);
    overflow-wrap: anywhere;
}

.trip-list {
    min-width: 0;
}

.trip-list__item {
    border-top: 1px solid var(--color-border);
}

.trip-list__link {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--space-md);
    min-width: 0;
    min-height: var(--touch-target);
    padding: var(--space-sm) 0;
    text-decoration: none;
    color: inherit;
}

.trip-list__body {
    display: block;
    flex: 1;
    min-width: 0;
}

/*
 * A place snapshot is a whole formatted address (§16.1). Two of them in full
 * made a three-line heading per row and four rows to a phone screen; clamped to
 * two lines the row is scannable and the text is still in the document, where a
 * screen reader and the trip's own screen both find it.
 */
.trip-list__route {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
    font-weight: var(--weight-label);
    overflow-wrap: anywhere;
}

/* A place a private trip did not record is not a place: it reads as the absence
   it is, rather than in the same weight as a real destination (§13.4). */
.trip-list__unknown {
    color: var(--color-text-muted);
    font-weight: var(--weight-body);
}

.trip-list__facts {
    display: block;
    margin-top: var(--space-xs);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

.trip-list__tags {
    display: block;
    margin-top: var(--space-xs);
}

.notice__list {
    margin: var(--space-xs) 0 0;
    padding-left: var(--space-lg);
}

/* Logbook views, month navigation and calendar (§25.2) ---------------------- */

/* The Logbook's three views, as the same segmented control the vehicle
   switcher uses: one track, one chosen segment (§25.2). */
.view-switch {
    min-width: 0;
}

.view-switch__list {
    display: flex;
    flex-direction: row;
    gap: var(--space-xs);
    padding: var(--space-xs);
    border-radius: var(--radius-pill);
    background: var(--color-surface-sunken);
}

.view-switch__item {
    flex: 1 1 0;
    min-width: 0;
}

.view-switch__link {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    min-height: var(--touch-target);
    padding: var(--space-xs) var(--space-sm);
    border-radius: var(--radius-pill);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    text-align: center;
    text-decoration: none;
    transition: background-color var(--duration-fast) var(--ease-standard);
}

/* The current view is marked by weight and by being lifted out of the track as
   well as by colour (§25.8: never by colour alone). */
.view-switch__link.is-active {
    background: var(--color-surface);
    box-shadow: var(--elev-1);
    color: var(--color-text);
    font-weight: var(--weight-label);
}

.month-nav {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
}

.month-nav__label {
    margin: 0;
    font-weight: var(--weight-title);
    font-variant-numeric: tabular-nums;
}

.logbook__summary {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

/* A seven-column grid has to fit inside 320 CSS pixels, so the cells carry the
   day number and a compact figure and nothing else (§25.8). */

/* The grid is as wide as the panel allows: at 320 CSS pixels seven columns
   cannot each be 44 wide, so the cells give back the panel's side padding and
   take the rest. See the M11 verification record for the measured figure and
   why this is a considered exception to §25.8's target size — every day is also
   reachable through the all-trips view, which does meet it. */
.calendar {
    width: calc(100% + 2 * var(--surface-pad) - 2 * var(--space-sm));
    margin: 0 calc(-1 * var(--surface-pad) + var(--space-sm));
    border-collapse: collapse;
    table-layout: fixed;
    font-size: var(--text-xs);
}

.calendar__caption {
    margin-bottom: var(--space-sm);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    text-align: left;
}

.calendar th {
    padding: var(--space-xs) 0;
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-label);
}

.calendar th abbr {
    text-decoration: none;
}

.calendar__cell {
    padding: 1px;
    border: 1px solid var(--color-border);
    vertical-align: top;
}

.calendar__link,
.calendar__empty,
.calendar__cell > .calendar__day {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1px;
    /* The whole cell is the target; seven of them fit in 320 pixels (§25.8).
       Every cell is the same height, so a week with a busy day does not stand
       two lines taller than the week above it. */
    min-height: 2.75rem;
    padding: var(--space-xs) 1px;
    text-decoration: none;
}

.calendar__cell > .calendar__day,
.calendar__empty {
    color: var(--color-text-muted);
}

/* A day that holds trips is filled, not merely darker: the fill is the accent's
   own tint, so a day with something in it reads as active rather than as
   shaded. The day number and the count carry the meaning in words (§25.8). */
.calendar__link {
    background: var(--color-accent-soft);
    color: var(--color-text);
    font-weight: var(--weight-title);
}

/* How many trips, as a number. The sentence it stands for is beside it in
   `.visually-hidden`, because a cell 40 pixels wide has room for neither. */
.calendar__count {
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-body);
    font-variant-numeric: tabular-nums;
    line-height: 1;
    text-align: center;
}

/* Wide viewports (ADR-0066) ------------------------------------------------- */
/*
 * A desktop is not a stretched phone. The column stays mobile-first and every
 * breakpoint is a `min-width`; what the extra width buys is *structure* — more
 * figures in a row, two columns of rows, two column-pairs of facts — and never
 * a longer line of prose or a wider input. `--measure` and `--measure-control`
 * are the two ceilings that make that true.
 */

@media (min-width: 48rem) {
    /* A label column that is a fraction of the page wraps on every detail
       screen; one that is a fixed measure does not. */
    .data-list,
    .vehicle-list__data {
        grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
    }

    .form .field__input {
        max-width: var(--measure-control);
    }

    .figures {
        gap: var(--space-lg);
    }
}

@media (min-width: 64rem) {
    .app-main {
        max-width: var(--content-max-wide);
        padding-top: var(--space-2xl);
        padding-left: var(--space-lg);
        padding-right: var(--space-lg);
    }

    /* The four numbers a driver opens the app for, in one row (§26.1). */
    .figures {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }

    /* Rows are short; two columns of them show a whole master-data list or a
       whole menu without scrolling. */
    .menu-list,
    .record-list {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        column-gap: var(--space-xl);
    }

    /* A section heading spans both columns of the list below it. */
    .panel > .panel__title {
        grid-column: 1 / -1;
    }
}
