/*
    ServiceRound stylesheet.

    Deliberately not a CSS framework. The template's Bootstrap reference was removed before the first
    commit, because adding a framework later is a one-line change while removing one after forty
    components have been built against its class names is not.

    WHO THIS IS DESIGNED FOR, because it drives nearly every decision below.

    A valeter, on a driveway, in daylight, holding a phone in one hand and something wet in the
    other. That means: large touch targets, real contrast rather than fashionable grey-on-grey, the
    primary action within thumb reach at the BOTTOM of the screen rather than the top corner, and
    text that does not need a second look. Everything here is sized for a phone first and allowed to
    breathe on a desktop, not the other way round.

    TWO COLOURS ARE NOT DEFINED HERE. --brand-primary and --brand-accent are set at runtime by
    MainLayout from the tenant's branding, because this file is the same for every customer and those
    colours are not.
*/

/* =================================================================================================
   Tokens
   ================================================================================================= */

:root {
    /* Fallbacks only. The real values are applied per tenant on .app-shell. */
    --brand-primary: #272727;
    --brand-accent: #1c1917;
    /* Black or white, computed from the accent's luminance at runtime - see BrandingStyle. A
       hardcoded value here works for one brand colour and fails for every other. */
    --brand-accent-ink: #ffffff;

    /*
        A WARM neutral scale rather than a blue-grey one. The brand is charcoal and gold, and cool
        greys next to gold look accidental - slightly warm greys look chosen.
    */
    --grey-50:  #fafaf9;
    --grey-100: #f5f5f4;
    --grey-200: #e7e5e4;
    --grey-300: #d6d3d1;
    --grey-400: #a8a29e;
    --grey-500: #78716c;
    --grey-600: #57534e;
    --grey-700: #44403c;
    --grey-800: #292524;
    --grey-900: #1c1917;

    --surface:          #ffffff;
    --surface-sunken:   var(--grey-50);
    --surface-dark:     #17181a;
    --surface-dark-raised: #1f2124;

    /*
        Text colours chosen for a phone screen in daylight, not a designer's monitor in a dark room.
        --text-secondary is grey-600 rather than the usual grey-400: it reaches about 7:1 against
        white, and the difference between 4.5:1 and 7:1 is the difference between readable and
        legible when the sun is on the screen.
    */
    --text-primary:   var(--grey-900);
    --text-secondary: var(--grey-600);
    --text-tertiary:  var(--grey-500);
    --text-on-dark:         #f5f5f4;
    --text-on-dark-muted:   #c7c3bf;

    --border:        var(--grey-200);
    --border-strong: var(--grey-300);

    --danger:  #b42318;
    --danger-surface: #fef3f2;
    --warning: #b54708;
    --warning-surface: #fffaeb;

    /* A four-pixel base. Every margin and padding below is a multiple, so vertical rhythm holds
       without anybody having to think about it. */
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-5: 1.5rem;
    --space-6: 2rem;
    --space-7: 3rem;
    --space-8: 4rem;

    --radius-sm: 6px;
    --radius:    10px;
    --radius-lg: 16px;
    --radius-full: 999px;

    /* Layered rather than one shadow, so raised things read as raised instead of blurred. */
    --shadow-1: 0 1px 2px rgba(28, 25, 23, 0.06), 0 1px 3px rgba(28, 25, 23, 0.10);
    --shadow-2: 0 4px 6px -2px rgba(28, 25, 23, 0.05), 0 12px 16px -4px rgba(28, 25, 23, 0.10);
    --shadow-3: 0 8px 8px -4px rgba(28, 25, 23, 0.04), 0 20px 24px -4px rgba(28, 25, 23, 0.14);

    /* The one thing every interactive element must clear. 48px is the figure that actually gets hit
       reliably by a cold or wet thumb; 44 is the documented minimum and is not enough here. */
    --touch-target: 3rem;

    --header-height: 3.5rem;
}

/* =================================================================================================
   Reset and base
   ================================================================================================= */

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

/* =================================================================================================
   Inter
   =================================================================================================
   SELF-HOSTED, not linked from Google Fonts. This application has to open on a driveway with no
   signal, and a stylesheet that fetches its font from somebody else's server is a stylesheet that
   renders in Segoe UI on the one day it matters. It is cached by the service worker with everything
   else, which a third-party origin would not be.

   The variable font rather than four static weights: one 344KB file covers every weight from 100 to
   900, where the static cut of regular, medium, semibold and bold is four requests and more bytes
   for less range. It also means a heading can sit at 620 rather than being forced to choose between
   600 and 700.

   WHY A FONT AT ALL. Everything was system-ui, which on Windows is Segoe UI - the typeface of every
   internal line-of-business tool ever built, and the fastest way for software to read as something
   assembled rather than something bought. It is the single largest change available to how finished
   this looks, and it costs one file.
*/
@font-face {
    font-family: "Inter";
    font-style: normal;
    /* The whole variable range, so font-weight anywhere in it renders as drawn rather than as a
       browser's synthetic approximation. */
    font-weight: 100 900;
    font-display: swap;
    src: url("../fonts/InterVariable.woff2") format("woff2");
}

@font-face {
    font-family: "Inter";
    font-style: italic;
    font-weight: 100 900;
    font-display: swap;
    src: url("../fonts/InterVariable-Italic.woff2") format("woff2");
}

/* =================================================================================================
   Country flags
   =================================================================================================
   Windows ships no font for flag emoji. Without this, Chrome on a Windows desktop draws the two
   letters "GB" in little boxes instead of a flag, so the dropdown that is supposed to be scannable
   by shape becomes a column of identical grey rectangles.

   Scoped by unicode-range to the regional indicator block, so the browser fetches these 76KB only
   when a flag character is actually on a page - which is the customer form and nowhere else. It is
   listed FIRST in every font stack that needs it, so it wins for those characters and nothing else.
*/
@font-face {
    font-family: "Twemoji Country Flags";
    unicode-range: U+1F1E6-1F1FF;
    src: url("../fonts/TwemojiCountryFlags.woff2") format("woff2");
    font-display: swap;
}

html {
    /* Anchored jumps land below the sticky header rather than underneath it. */
    scroll-padding-top: calc(var(--header-height) + var(--space-4));
}

html,
body {
    margin: 0;
    padding: 0;
    min-height: 100%;
    background-color: var(--surface-dark);
    color: var(--text-primary);
    font-family: "Twemoji Country Flags", "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    /*
        Contextual alternates and tabular figures on, everywhere.

        cv11 gives Inter's single-storey lowercase L, which stops "l" and "1" and "I" being three
        near-identical vertical strokes - on a screen carrying registration plates and prices that
        matters more than it sounds.

        Tabular figures are set on the elements that need columns to line up rather than globally, so
        ordinary prose keeps its proportional spacing.
    */
    font-feature-settings: "cv11", "ss01";
    font-optical-sizing: auto;
    font-size: 1rem;
    line-height: 1.5;
    /* Stops iOS inflating text in landscape, which otherwise breaks a carefully sized layout the
       moment somebody turns their phone. */
    -webkit-text-size-adjust: 100%;
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
    margin: 0;
    line-height: 1.25;
    /* Tightened, because system fonts are set loose at large sizes and headings otherwise look
       accidental next to the wordmark in the logo. */
    letter-spacing: -0.02em;
}

p { margin: 0; }

a { color: inherit; }

button, input, select, textarea { font: inherit; color: inherit; }

/*
    One focus style for everything, and it is deliberately loud.

    :focus-visible rather than :focus, so a ring appears for keyboard users and never for a thumb -
    which is what stops a focus ring reading as a stray border on a phone.
*/
:focus-visible {
    outline: 3px solid var(--brand-accent);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* =================================================================================================
   Shell
   ================================================================================================= */

.app-shell {
    display: flex;
    flex-direction: column;
    min-height: 100dvh;
    background-color: var(--surface-sunken);
}

.app-main {
    flex: 1;
    display: flex;
    flex-direction: column;
}

.environment-banner {
    flex: 0 0 auto;
    padding: var(--space-1) var(--space-4);
    padding-top: max(var(--space-1), env(safe-area-inset-top));
    color: #ffffff;
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-align: center;
    text-transform: uppercase;
}

/* --- Top bar ---------------------------------------------------------------------------------- */

.topbar {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    min-height: var(--header-height);
    padding: var(--space-2) max(var(--space-4), env(safe-area-inset-right))
             var(--space-2) max(var(--space-4), env(safe-area-inset-left));
    background-color: var(--surface-dark);
    color: var(--text-on-dark);
}

.topbar__brand {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    text-decoration: none;
    min-height: var(--touch-target);
}

.topbar__logo {
    height: 26px;
    width: auto;
    display: block;
}

/*
    The company name, always visible.

    Not decoration. One person can hold accounts at two valeting companies - that is the case that
    made ASP.NET Core Identity's schema unusable here - so "which company am I looking at" is a
    question the interface has to answer without being asked. Realising you are in the wrong one by
    recognising a customer's name is far too late.
*/
.topbar__tenant {
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--text-on-dark);
    letter-spacing: 0.01em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 38vw;
}

.topbar__actions { display: flex; align-items: center; gap: var(--space-2); }

/*
    Deliberately NOT the tenant's accent colour.

    It sits on the dark top bar, and an accent chosen to stand out against a white page is not
    guaranteed to stand out against near-black - a dark brand colour makes this chip vanish
    entirely, initials and all. A translucent white overlay works against any accent whatsoever,
    because it is defined relative to whatever is behind it rather than to a colour a tenant picked.
*/
.topbar__user {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    border-radius: var(--radius-full);
    background-color: rgba(255, 255, 255, 0.14);
    color: var(--text-on-dark);
    font-size: 0.75rem;
    font-weight: 700;
}

.topbar__signout {
    display: inline-flex;
    align-items: center;
    min-height: 2.25rem;
    padding: 0 var(--space-3);
    background: none;
    border: 1px solid rgba(255, 255, 255, 0.22);
    border-radius: var(--radius-full);
    color: var(--text-on-dark);
    font-size: 0.8125rem;
    cursor: pointer;
    transition: background-color 0.15s ease;
}

.topbar__signout:hover { background-color: rgba(255, 255, 255, 0.08); }

/* Quieter than the sign-out button, because it is a place to go rather than an action, and the two
   sitting side by side with equal weight makes the bar read as a row of buttons. */
.topbar__link {
    display: inline-flex;
    align-items: center;
    min-height: 2.25rem;
    padding: 0 var(--space-2);
    color: var(--text-on-dark);
    text-decoration: none;
    font-size: 0.8125rem;
    opacity: 0.85;
    border-radius: var(--radius-sm);
}

.topbar__link:hover { opacity: 1; background-color: rgba(255, 255, 255, 0.08); }

/* =================================================================================================
   Page
   ================================================================================================= */

.page {
    flex: 1;
    width: 100%;
    /*
        ONE WIDTH FOR EVERY SCREEN.

        There used to be two - a reading width for forms and a wider one for lists - and six of the
        ten screens had already taken the wider one. Two widths meant a visible jump every time
        somebody moved between a list and the form that edits its rows, which reads as the page
        having reloaded into something else.

        The cost of a wide page is text boxes stretching to silly lengths, and that is dealt with
        below by capping the inputs rather than the page: cards get the room, prose does not.
    */
    max-width: 68rem;
    margin: 0 auto;
    padding: var(--space-5) max(var(--space-4), env(safe-area-inset-right))
             calc(var(--space-8) + env(safe-area-inset-bottom)) max(var(--space-4), env(safe-area-inset-left));
}



/*
    A wide page widens the CARDS, not the text boxes.

    Grids of cards - services, size bands, vehicles, customers - want the room. A text box does not:
    at the full width of this page an email address sat in a box a thousand pixels long, which is
    hard to scan, hard to check for a typo, and looks like a mistake.

    Capped rather than laid out in columns because a form read top to bottom in one column is easier
    to fill in one-handed, which is how this is used. The exceptions below are the fields that
    genuinely want the width.
*/
.page .field > .input,
.page .field > .input-group { max-width: 34rem; }

/* A note can run long and is written in, not scanned - so it keeps the room. */
.page .field > textarea.input { max-width: none; }

/*
    Title first, actions after it.

    The markup used to put Cancel first and space-between pushed the heading to the far right, so the
    page announced itself from the wrong end with a corridor of nothing across the middle. A title
    belongs at the start of the thing it titles.
*/
.page__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    margin-bottom: var(--space-4);
}

.page__header-actions { display: flex; align-items: center; gap: var(--space-2); flex: 0 0 auto; }

.page__title {
    font-size: 1.75rem;
    font-weight: 680;
    /* Tightened, which a well-drawn face can carry and a system font cannot. At display size the
       default spacing reads as loose and slightly unfinished. */
    letter-spacing: -0.02em;
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    /*
        Room under the heading, which it did not have.

        Pages that wrap their content in a .form got away with it, because that carries its own gap.
        The booking screen did not, so the back link, the title, the section heading and the field
        label stacked up in about a hundred pixels with a screenful of nothing underneath - four
        levels of hierarchy with no space to tell them apart.
    */
    margin: 0 0 var(--space-5);
}

.muted { color: var(--text-secondary); font-size: 0.875rem; }

.back {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--touch-target);
    /* Its own line, with room under it. Without this the heading starts at the exact pixel the link
       ends, and the two read as one block of text rather than as a way out and a title. */
    margin-bottom: var(--space-2);
    color: var(--text-secondary);
    font-size: 0.875rem;
    font-weight: 500;
    text-decoration: none;
}

.back:hover { color: var(--text-primary); }

/* =================================================================================================
   Search
   ================================================================================================= */

/*
    Sticky, directly under the top bar.

    Search IS the customer screen - it is why somebody opened the application - and scrolling a list
    of forty customers should never take the means of narrowing it off the screen.
*/
.search {
    position: sticky;
    top: var(--header-height);
    z-index: 10;
    margin: 0 calc(-1 * var(--space-4)) var(--space-4);
    padding: var(--space-3) var(--space-4);
    background-color: var(--surface-sunken);
}

.search__field { position: relative; display: flex; align-items: center; }

.search__icon {
    position: absolute;
    left: var(--space-3);
    color: var(--text-tertiary);
    pointer-events: none;
}

.search__input {
    width: 100%;
    min-height: var(--touch-target);
    /* 16px minimum, non-negotiable: anything smaller makes iOS Safari zoom the page the moment the
       field is focused, which throws the whole layout sideways every time somebody starts typing. */
    font-size: 16px;
    padding: var(--space-3) var(--space-3) var(--space-3) 2.75rem;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    background-color: var(--surface);
    box-shadow: var(--shadow-1);
    -webkit-appearance: none;
    appearance: none;
}

.search__input::placeholder { color: var(--text-tertiary); }

.search__input:focus {
    border-color: var(--brand-accent);
    box-shadow: var(--shadow-1), 0 0 0 3px color-mix(in srgb, var(--brand-accent) 25%, transparent);
    outline: none;
}

/* =================================================================================================
   Customer list
   ================================================================================================= */

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

/*
    Initials rather than a photograph. Scanning a list of names is faster with a consistent shape to
    anchor each row, and a valeting company has no photographs of its customers - nor should it.
*/
.avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    flex-shrink: 0;
    border-radius: var(--radius-full);
    background-color: var(--grey-100);
    color: var(--text-secondary);
    font-size: 0.8125rem;
    font-weight: 700;
}

.avatar--business { background-color: color-mix(in srgb, var(--brand-primary) 12%, white); color: var(--brand-primary); }

.avatar--large { width: 3.5rem; height: 3.5rem; font-size: 1.125rem; }

/* The customer card is defined in "The customer card" section below - it became a grid with a
   fixed plate column when vehicles gained their own rows. */

.customer-card__chevron { color: var(--grey-400); flex-shrink: 0; }

/* --- Registration plate ----------------------------------------------------------------------- */

/*
    Drawn as an actual number plate. This is the single most recognisable thing on the screen when
    somebody is comparing what is in their hand against the car in front of them, and abstracting it
    into ordinary text would throw away the one visual match the eye can make instantly.
*/
.plate {
    display: inline-block;
    /*
        Inter at 800 with tabular figures, rather than the monospace stack this used to carry.

        The weight is the point: this was already at 700, which is as heavy as Consolas is drawn, so
        asking for more would have got a browser-synthesised fake bold - the same outline smeared
        slightly wider, which looks blurry rather than bolder. Inter is a variable font with a real
        800 in it.

        Tabular figures keep the uniform character widths that the monospace stack was chosen for, so
        nothing is lost. It also happens to be closer to a real plate: the typeface on the back of a
        car is heavy and near-uniform, not a coding font.
    */
    font-family: "Inter", system-ui, sans-serif;
    font-weight: 800;
    font-feature-settings: "tnum";
    font-size: 0.8125rem;
    letter-spacing: 0.06em;
    background-color: #ffd800;
    color: #111111;
    padding: 0.15rem 0.4rem;
    border-radius: 4px;
    border: 1px solid #d9b800;
    white-space: nowrap;
}

.plate--large { font-size: 1rem; padding: var(--space-1) var(--space-3); }

/* Smaller, for a row where two or three sit beside a name. Full size there turns a search result
   into a wall of yellow and buries the name somebody is actually reading. */
.plate--small { font-size: 0.6875rem; padding: 0.1rem 0.3rem; letter-spacing: 0.04em; }

/* The input somebody types a plate into. Not the yellow plate itself - a yellow input box reads as a
   warning - but the same monospaced, spaced-out letterforms, so what is typed lines up visually with
   what is printed on the back of the car being looked at. */
.plate-input {
    font-family: "Inter", system-ui, sans-serif;
    font-weight: 700;
    font-feature-settings: "tnum";
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

/* --- Badges ----------------------------------------------------------------------------------- */

.badge {
    display: inline-flex;
    align-items: center;
    font-size: 0.6875rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding: 0.15rem var(--space-2);
    border-radius: var(--radius-full);
    background-color: var(--grey-100);
    color: var(--text-secondary);
    white-space: nowrap;
}

.badge--business { background-color: color-mix(in srgb, var(--brand-primary) 10%, white); color: var(--brand-primary); }

.badge--muted { background-color: var(--grey-100); color: var(--text-tertiary); }

/* Job status. Colour rather than text alone, because a list of dates all reading "Scheduled",
   "Completed", "Cancelled" is scanned by shape long before it is read. */
.badge--success { background-color: color-mix(in srgb, #15803d 10%, white); color: #15803d; }
.badge--info { background-color: color-mix(in srgb, #1d4ed8 9%, white); color: #1d4ed8; }

/* Something temporary that somebody may need to explain. A locked-out account clears itself, so this
   is amber rather than red - it is a thing to know about, not a thing to fix. */
.badge--warning { background-color: color-mix(in srgb, #b45309 10%, white); color: #b45309; }

/* =================================================================================================
   Empty and loading states
   ================================================================================================= */

.empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--space-3);
    padding: var(--space-7) var(--space-4);
}

.empty__icon { color: var(--grey-300); }

.empty__title { font-size: 1.0625rem; font-weight: 600; }

.empty__text { color: var(--text-secondary); font-size: 0.9375rem; max-width: 22rem; }

/*
    Skeletons rather than a spinner.

    A spinner says "wait"; a skeleton says "here is the shape of what is coming", which on a slow
    connection is the difference between a screen that feels broken and one that feels busy. It also
    stops the layout jumping when the results land.
*/
.skeleton-card {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    gap: var(--space-3);
    min-height: 4.5rem;
    padding: var(--space-3) var(--space-4);
    background-color: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.skeleton {
    background: linear-gradient(90deg, var(--grey-100) 25%, var(--grey-200) 37%, var(--grey-100) 63%);
    background-size: 400% 100%;
    animation: shimmer 1.4s ease infinite;
    border-radius: var(--radius-sm);
}

.skeleton--avatar { width: 2.5rem; height: 2.5rem; border-radius: var(--radius-full); flex-shrink: 0; }
.skeleton--line { height: 0.75rem; width: 70%; }
.skeleton--title { height: 1rem; width: 55%; margin-bottom: var(--space-2); }

@keyframes shimmer {
    0%   { background-position: 100% 50%; }
    100% { background-position: 0 50%; }
}

/* =================================================================================================
   Buttons
   ================================================================================================= */

.button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: var(--touch-target);
    padding: var(--space-3) var(--space-5);
    border: 1px solid transparent;
    border-radius: var(--radius);
    font-size: 1rem;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, transform 0.06s ease;
}

.button:active:not(:disabled) { transform: translateY(1px); }

.button--primary {
    background-color: var(--brand-accent);
    /* Chosen against the accent rather than fixed, so the label stays readable whether a tenant's
       colour is near-black or pale gold. */
    color: var(--brand-accent-ink);
    box-shadow: var(--shadow-1);
}

.button--primary:hover:not(:disabled) {
    background-color: color-mix(in srgb, var(--brand-accent) 88%, black);
    box-shadow: var(--shadow-2);
}

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

.button--secondary:hover:not(:disabled) { background-color: var(--grey-50); border-color: var(--grey-400); }

/*
    A button that reads as a button without competing with the one that matters.
    <para>
    THIS DID NOT EXIST, and three screens were already asking for it - so Cancel on the booking form
    rendered as a bare .button with padding and no surface at all. It read as stray text with a gap
    beside it, which somebody reasonably took for a missing icon.
    </para>
    Border but no fill: enough edge to be obviously pressable, quiet enough that "Cancel" never
    competes with "Book for today" sitting a few inches below it.
*/
.button--ghost {
    background-color: transparent;
    border-color: var(--border);
    color: var(--text-secondary);
}

.button--ghost:hover:not(:disabled) {
    background-color: var(--grey-50);
    border-color: var(--border-strong);
    color: var(--text-primary);
}

.button--block { width: 100%; }

.button--compact { min-height: 2.5rem; padding: var(--space-2) var(--space-4); font-size: 0.875rem; }

/* Only ever seen mid-action now - a form submitting, a save in flight - so it reads as "working"
   rather than "unavailable", and the label beside it says which. */
.button:disabled { opacity: 0.55; cursor: progress; box-shadow: none; }

/*
    A visible edge in the brand's own colour.

    On a monochrome palette a solid dark button on a white card is already unmistakable, but the
    sign-in card is the one screen with a single action on it and nothing else to compare against.
    A defined edge and a little more lift is what separates "a button" from "a coloured rectangle",
    and it costs nothing in restraint.
*/
.button--primary { border-color: var(--brand-accent); }

.button--primary:active:not(:disabled) { box-shadow: none; }

/*
    The primary button inverts on the dark landing backdrop.

    Pickett's accent is near-black, which is exactly right on a white card and invisible on a black
    panel - the one call to action on the first screen anybody sees was a dark slab on a dark
    background. The rule below already did this for the secondary button and the primary never got
    the same treatment.

    Inverted rather than given the accent a lighter variant: on a monochrome brand, a white button on
    black is the same gesture as a black button on white, and it stays right for a tenant whose
    accent is a colour.

    SCOPED TO .landing__actions, NOT TO .landing. The sign-in page uses the same dark backdrop, but
    its button sits inside a WHITE card - so inverting everything under .landing turned that one
    near-white on white and made it disappear entirely. Only the button sitting directly on the dark
    panel should flip.
*/
.landing__actions .button--primary {
    background-color: var(--text-on-dark);
    color: var(--brand-primary);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}

.landing__actions .button--primary:hover:not(:disabled) {
    background-color: #fff;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
}

/* On the dark landing backdrop the secondary button has to invert, or it becomes a white slab. */
.landing .button--secondary {
    background-color: transparent;
    border-color: rgba(255, 255, 255, 0.28);
    color: var(--text-on-dark);
}

.landing .button--secondary:hover { background-color: rgba(255, 255, 255, 0.08); }

/* --- Floating action button -------------------------------------------------------------------- */

/*
    Bottom RIGHT, not the top corner.

    The top-right corner is the hardest place on a phone to reach with the thumb of the hand holding
    it - and this is used one-handed, because the other hand is holding something. The primary
    action belongs where the thumb already is.

    Hidden on wider screens, where the header button is perfectly reachable and a floating circle
    would just be a phone convention imported for no reason.
*/
.fab {
    position: fixed;
    right: max(var(--space-4), env(safe-area-inset-right));
    bottom: calc(var(--space-4) + env(safe-area-inset-bottom));
    z-index: 30;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--touch-target);
    padding: 0 var(--space-5);
    border: none;
    border-radius: var(--radius-full);
    background-color: var(--brand-accent);
    color: var(--brand-accent-ink);
    font-size: 0.9375rem;
    font-weight: 700;
    text-decoration: none;
    box-shadow: var(--shadow-3);
}

.fab:active { transform: translateY(1px); }

.desktop-only { display: none; }

@media (min-width: 640px) {
    .fab { display: none; }
    .desktop-only { display: inline-flex; }
}

/* =================================================================================================
   Detail
   ================================================================================================= */

.detail-header {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    margin: var(--space-2) 0 var(--space-5);
}

.detail-header__body { min-width: 0; flex: 1; }

.detail-header__name { font-size: 1.5rem; font-weight: 680; letter-spacing: -0.02em; }

/*
    Call and email as full buttons rather than links in body text.

    On a driveway the action is "ring them", and a tel: link inside a paragraph is a small target for
    a wet thumb. These are the two things somebody actually does from this screen, so they get the
    space that implies.
*/
.actions-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
    gap: var(--space-2);
    margin-bottom: var(--space-5);
}

.card {
    background-color: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-1);
    padding: var(--space-4);
    margin-bottom: var(--space-3);
}

.card__heading {
    font-size: 0.6875rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-tertiary);
    margin-bottom: var(--space-3);
}

.field-row {
    display: flex;
    gap: var(--space-4);
    padding: var(--space-2) 0;
    border-bottom: 1px solid var(--grey-100);
}

.field-row:last-child { border-bottom: none; padding-bottom: 0; }

/*
    Sized to its content, and never wrapped.

    This was a fixed 5rem, which is narrower than "Found us via" - so that label broke across two
    lines and read as "Found us / via" stacked above its own value, which looks like a rendering
    fault rather than a deliberate layout. Nothing here depends on labels sharing a width: each
    card carries a single row, so there is no column for them to line up in.
*/
.field-row__label { flex: 0 0 auto; white-space: nowrap; color: var(--text-secondary); font-size: 0.875rem; }

.field-row__value { flex: 1; min-width: 0; word-break: break-word; }

.address { font-style: normal; line-height: 1.6; }

/*
    Access notes given their own treatment, deliberately louder than everything around them.

    This is the gate code, or where to park, or whether there is a dog. It is read while standing
    outside somebody's house in the rain, and it is the one thing on this screen that has to be found
    without hunting for it.
*/
.access-note {
    display: flex;
    gap: var(--space-3);
    margin-top: var(--space-3);
    padding: var(--space-3);
    background-color: var(--warning-surface);
    border: 1px solid color-mix(in srgb, var(--warning) 25%, transparent);
    border-radius: var(--radius-sm);
    font-size: 0.9375rem;
}

.access-note__icon { color: var(--warning); flex-shrink: 0; }

.notice {
    display: flex;
    gap: var(--space-3);
    padding: var(--space-3);
    background-color: var(--danger-surface);
    border: 1px solid color-mix(in srgb, var(--danger) 25%, transparent);
    border-radius: var(--radius-sm);
    color: var(--danger);
    font-size: 0.9375rem;
}

.vehicle {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--grey-100);
    flex-wrap: wrap;
}

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

.vehicle--sold { opacity: 0.6; }

.vehicle__name { font-weight: 600; }

/* =================================================================================================
   Forms
   ================================================================================================= */

.form { display: flex; flex-direction: column; gap: var(--space-5); }

.fieldset { border: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-3); }

.fieldset__legend {
    font-size: 0.6875rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-tertiary);
    padding: 0;
    /* A legend does not take part in the fieldset's flex gap, so without this it sits almost on top
       of the first field and the sections stop reading as sections. */
    margin-bottom: var(--space-3);
}

/*
    Separates one group of questions from the next.

    MARGIN, NOT PADDING, and that distinction was a visible bug rather than a nicety. A legend is
    painted at the fieldset's BORDER box, so padding pushes the fields down and leaves the legend
    exactly where it was - sitting on top of whatever the previous section ended with. Every heading
    on the booking screen overlapped the card above it.
*/
.fieldset + .fieldset { margin-top: var(--space-5); border-top: 1px solid var(--border); padding-top: var(--space-4); }

/* And the legend needs clearing off the border it is painted on, or it sits half in the line. */
.fieldset + .fieldset > .fieldset__legend { margin-top: calc(-1 * var(--space-1)); }

/* A section whose contents have not appeared yet takes no room at all - no gap, no separator, no
   heading. Otherwise the form carries a rule across it with empty space underneath. */
.fieldset--empty { margin-top: 0; padding-top: 0; border-top: none; gap: 0; }

.field { display: flex; flex-direction: column; gap: var(--space-2); flex: 1; min-width: 0; }

.field__label { font-size: 0.875rem; font-weight: 500; color: var(--text-primary); }

.field__row { display: flex; gap: var(--space-3); flex-wrap: wrap; }

.field--narrow { flex: 0 0 7.5rem; }

/* Wider than a plain narrow field, because it holds a flag, a dialling code and a country name -
   the code being there so somebody can match the number in front of them without knowing which
   country "+353" belongs to. */
.field--country { flex: 0 0 10rem; }

.input {
    width: 100%;
    min-height: var(--touch-target);
    /* A select does not inherit the page font in Chrome, and without this the country dropdown falls
       back to the system font - which on Windows has no flag glyphs, so the flags vanish. */
    font-family: inherit;
    color: inherit;
    /* 16px, for the same iOS zoom reason as the search box. */
    font-size: 16px;
    padding: var(--space-3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    background-color: var(--surface);
    -webkit-appearance: none;
    appearance: none;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

textarea.input { min-height: 5rem; resize: vertical; }

select.input {
    /* A chevron drawn inline, so the control matches the others rather than getting whatever the
       operating system feels like drawing. */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2378716c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right var(--space-3) center;
    padding-right: var(--space-6);
}

.input:focus {
    border-color: var(--brand-accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-accent) 25%, transparent);
    outline: none;
}

.input::placeholder { color: var(--text-tertiary); }

.field__hint { font-size: 0.8125rem; color: var(--text-secondary); }

.field__error { font-size: 0.875rem; color: var(--danger); font-weight: 500; }

/* --- Segmented control -------------------------------------------------------------------------- */

/*
    A person and a business need different fields, so the choice comes first and the form changes to
    match. Asking for a company name and a first name at the same time produces records that are
    half of each.
*/
.segmented {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-1);
    padding: var(--space-1);
    background-color: var(--grey-100);
    border-radius: var(--radius);
}

.segmented__option {
    min-height: 2.75rem;
    border: none;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text-secondary);
    font-size: 0.9375rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}

.segmented__option--selected {
    background-color: var(--surface);
    color: var(--text-primary);
    box-shadow: var(--shadow-1);
}

/* --- Sticky save ------------------------------------------------------------------------------- */

/*
    Pinned to the bottom on a phone.

    A long form scrolls the save button off the screen, and "where has the button gone" is a
    genuinely common way for somebody to abandon a half-typed customer.
*/
.form-actions {
    position: sticky;
    bottom: 0;
    margin: var(--space-2) calc(-1 * var(--space-4)) calc(-1 * var(--space-8));
    padding: var(--space-4) var(--space-4) calc(var(--space-4) + env(safe-area-inset-bottom));
    background-color: color-mix(in srgb, var(--surface-sunken) 92%, transparent);
    backdrop-filter: blur(8px);
    border-top: 1px solid var(--border);
}

/* =================================================================================================
   Landing and sign-in
   ================================================================================================= */

.landing {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-6);
    padding: var(--space-6) max(var(--space-4), env(safe-area-inset-right))
             max(var(--space-5), env(safe-area-inset-bottom)) max(var(--space-4), env(safe-area-inset-left));

    /*
        Dark, and not by preference. Pickett's logo is white artwork on a transparent background, so
        on a white page it would be invisible - the customer's identity would be a blank rectangle.

        Three layers: a faint wash of the accent at the top to give the eye somewhere to go, the
        tenant's primary colour beneath so a brand with real colour shows through, and a base
        gradient keeping the bottom dark so the footer recedes. color-mix throughout, so any brand
        colour still produces a backdrop a light-on-dark logo reads against.
    */
    background:
        radial-gradient(90% 55% at 50% -5%, color-mix(in srgb, var(--brand-accent) 16%, transparent) 0%, transparent 60%),
        radial-gradient(120% 90% at 50% 0%, color-mix(in srgb, var(--brand-primary) 75%, transparent) 0%, transparent 72%),
        linear-gradient(180deg, var(--surface-dark-raised) 0%, var(--surface-dark) 100%);
    color: var(--text-on-dark);
}

.landing__panel {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-5);
    width: 100%;
    max-width: 24rem;
    text-align: center;
    margin: auto 0;
}

.landing__logo { width: clamp(190px, 52vw, 280px); height: auto; }

.landing__logo--small { width: clamp(150px, 38vw, 200px); }

/* A hairline in the tenant's accent. Small, and the one place the brand colour appears at full
   strength, which is what keeps a monochrome page from looking unfinished. */
.landing__rule {
    width: 3.5rem;
    height: 2px;
    background-color: var(--brand-accent);
    border-radius: 2px;
    opacity: 0.9;
}

.landing__tagline {
    max-width: 20rem;
    color: var(--text-on-dark-muted);
    font-size: 1rem;
}

.landing__footer {
    margin-top: auto;
    color: var(--text-on-dark-muted);
    font-size: 0.6875rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-align: center;
}

.landing__offline { display: block; text-transform: none; letter-spacing: normal; color: #fbbf24; margin-top: var(--space-1); }

/*
    The sign-in form as a light card on the dark backdrop.

    Fields floating directly on a dark gradient looked unfinished - white inputs on near-black with
    nothing tying them together. A card gives them somewhere to sit, and it is the pattern people
    already recognise as "this is the part you fill in".
*/
.auth-card {
    width: 100%;
    padding: var(--space-5);
    background-color: var(--surface);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-3);
    color: var(--text-primary);
    text-align: left;
}

.auth-card__title { font-size: 1.25rem; font-weight: 700; margin-bottom: var(--space-1); }

.auth-card__subtitle { color: var(--text-secondary); font-size: 0.875rem; margin-bottom: var(--space-5); }

.auth-card .form { gap: var(--space-4); }

.tenant-choice {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    width: 100%;
    min-height: var(--touch-target);
    padding: var(--space-3) var(--space-4);
    background-color: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    font-size: 1rem;
    font-weight: 600;
    text-align: left;
    cursor: pointer;
    transition: border-color 0.15s ease, background-color 0.15s ease;
}

.tenant-choice:hover:not(:disabled) { border-color: var(--brand-accent); background-color: var(--grey-50); }

/* =================================================================================================
   Utilities
   ================================================================================================= */

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

.icon { width: 1.25rem; height: 1.25rem; flex-shrink: 0; }
.icon--sm { width: 1rem; height: 1rem; }
.icon--lg { width: 2.5rem; height: 2.5rem; }

/* =================================================================================================
   Blazor's own markup
   ================================================================================================= */
/* The ring drawn while the WebAssembly runtime downloads. Blazor sets --blazor-load-percentage
   itself as assemblies arrive. Styled against the dark backdrop the landing page uses, so the first
   paint and the loaded page do not look like two different applications. */

.loading-progress {
    position: relative;
    display: block;
    width: 8rem;
    height: 8rem;
    margin: 20vh auto var(--space-4) auto;
}

.loading-progress circle {
    fill: none;
    stroke: rgba(255, 255, 255, 0.12);
    stroke-width: 0.4rem;
    transform-origin: 50% 50%;
    transform: rotate(-90deg);
}

.loading-progress circle:last-child {
    stroke: var(--brand-accent);
    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: 600;
    color: var(--text-on-dark);
    inset: calc(20vh + 3.25rem) 0 auto 0;
}

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

#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1000;
    padding: var(--space-3) var(--space-5);
    background: var(--danger);
    color: #ffffff;
    box-shadow: var(--shadow-3);
}

#blazor-error-ui .dismiss { cursor: pointer; position: absolute; right: var(--space-3); top: var(--space-2); }

#blazor-error-ui .reload { color: #ffffff; }

/*
    Blazor's router focuses the h1 on navigation so a screen reader announces the new page. That is
    worth keeping - but programmatic focus counts as :focus-visible in Chrome, so the global focus
    ring lands on a heading nobody tabbed to and reads as a stray gold border.

    Targeted at tabindex="-1", which is exactly what FocusOnNavigate adds and what nothing genuinely
    interactive has.
*/
[tabindex="-1"]:focus,
[tabindex="-1"]:focus-visible { outline: none; }


/* =================================================================================================
   Contacts at a business
   =================================================================================================
   A business is not a person with a company name. A garage has a service manager who books the work
   and a receptionist who pays for it, and "the number we have for Colchester Motors" is no help when
   a van is ready and nobody is picking up. Each person is a card rather than another row of inputs,
   so it is obvious where one person ends and the next begins on a phone screen. */

.contact-card {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-4);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    background-color: var(--surface);
}

.contact-card__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
}

.contact-card__title {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--text-secondary);
    letter-spacing: 0.02em;
}

/* Deliberately quiet. Removing somebody is a real action that should be available without being
   the most eye-catching thing in the card. */
.contact-card__remove {
    border: none;
    background: none;
    padding: var(--space-2);
    margin: calc(-1 * var(--space-2));
    color: var(--text-secondary);
    cursor: pointer;
    border-radius: var(--radius-sm);
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    font-size: 0.875rem;
}

.contact-card__remove:hover { color: var(--danger); background-color: var(--surface-dark); }

/* A checkbox rather than a radio group, because there is only ever one question being asked of each
   card - "is this the one to ring first" - and the code guarantees the answer is unique. */
.contact-card__primary {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: 0.875rem;
    color: var(--text-secondary);
    min-height: var(--touch-target);
}

.contact-card__primary input { width: 1.125rem; height: 1.125rem; accent-color: var(--brand-accent); }

.contacts { display: flex; flex-direction: column; gap: var(--space-3); }

/* The postcode lookup's answer. Reserved space is not used - the message appears only when there is
   one - because a permanently empty line under a field reads as something having gone wrong. */
.field__lookup { font-size: 0.8125rem; color: var(--text-secondary); display: flex; align-items: center; gap: var(--space-2); }
.field__lookup--found { color: var(--text-primary); }

/* =================================================================================================
   Choosing things on a phone
   =================================================================================================
   This application is used on a phone almost all of the time, standing next to a car, often
   one-handed and often in the rain. Everything below is built for a thumb first and a mouse second:
   nothing relies on hover, every target clears the 48px minimum, and single-choice lists are laid
   out as things you tap rather than dropdowns you have to open, scroll and confirm.

   Desktop gets the same controls, wider. That is deliberate - one layout that scales beats two that
   drift apart. */

/* Where the lead came from. Pills rather than a select: there are nine answers, they are all short,
   and showing them at once means one tap instead of open-scroll-tap-dismiss. */
.chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }

.chip {
    min-height: var(--touch-target);
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-full);
    background-color: var(--surface);
    color: var(--text-primary);
    font-size: 0.9375rem;
    cursor: pointer;
    transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}

.chip--selected {
    background-color: var(--brand-accent);
    border-color: var(--brand-accent);
    color: var(--brand-accent-ink);
    font-weight: 500;
}

/* Vehicle size. Two columns rather than a four-across segmented control, because "Extra Large" does
   not fit across a 375px screen and abbreviating it to XL makes somebody guess. The example under
   each name is what actually tells a valeter which band a customer's car is in. */
.size-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-2); }

.size-option {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    min-height: var(--touch-target);
    padding: var(--space-3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    background-color: var(--surface);
    color: var(--text-primary);
    text-align: left;
    cursor: pointer;
    transition: background-color 0.12s ease, border-color 0.12s ease;
}

.size-option--selected { border-color: var(--brand-accent); background-color: var(--grey-50); box-shadow: inset 0 0 0 1px var(--brand-accent); }

.size-option__name { font-weight: 600; font-size: 0.9375rem; }
.size-option__example { font-size: 0.8125rem; color: var(--text-secondary); }

/* =================================================================================================
   Building a package
   ================================================================================================= */

/* A switch rather than a checkbox, because "am I taking a booking or just filing this person" is the
   single biggest branch on the form and it needs to read as an on/off state from across a forecourt. */
.switch-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    min-height: var(--touch-target);
    cursor: pointer;
}

.switch-row__text { display: flex; flex-direction: column; gap: 2px; }
.switch-row__label { font-weight: 500; }
.switch-row__hint { font-size: 0.8125rem; color: var(--text-secondary); }

.switch {
    flex: 0 0 3.25rem;
    height: 1.875rem;
    border-radius: var(--radius-full);
    background-color: var(--grey-300);
    border: none;
    padding: 0;
    position: relative;
    cursor: pointer;
    transition: background-color 0.15s ease;
}

.switch::after {
    content: "";
    position: absolute;
    top: 3px;
    left: 3px;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 50%;
    background-color: #fff;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
    transition: transform 0.15s ease;
}

.switch--on { background-color: var(--brand-accent); }
.switch--on::after { transform: translateX(1.375rem); }

/* One card per service, full width and stacked. A grid of small cards would fit more on a desktop
   and would be unusable on the screen this is actually used on. */
.service-list { display: flex; flex-direction: column; gap: var(--space-2); }

.service-card {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-3);
    width: 100%;
    min-height: var(--touch-target);
    padding: var(--space-4);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    background-color: var(--surface);
    color: var(--text-primary);
    text-align: left;
    cursor: pointer;
    transition: border-color 0.12s ease, background-color 0.12s ease;
}

.service-card--selected { border-color: var(--brand-accent); box-shadow: inset 0 0 0 1px var(--brand-accent); }

.service-card__body { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.service-card__name { font-weight: 600; }

/* Clamped rather than truncated to one line: the description is what a customer is being sold, and
   two lines is enough to tell a full valet from an exterior deep clean without turning the list into
   a wall of text. */
.service-card__description {
    font-size: 0.8125rem;
    color: var(--text-secondary);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.service-card__meta { font-size: 0.75rem; color: var(--text-tertiary); }

/* The price never wraps and never shrinks. It is the number the customer is about to be quoted, and
   a long service name must not be allowed to push it onto a second line where it reads as part of
   the description. */
.service-card__price {
    flex: 0 0 auto;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.service-card__price--quoted { font-size: 0.8125rem; font-weight: 500; color: var(--text-secondary); }

/* The running total, pinned above the save button. Somebody reading a price out over the phone
   should never have to scroll to find it. */
.total-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3) 0;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.total-bar__label { font-weight: 500; color: var(--text-secondary); }
.total-bar__note { font-size: 0.8125rem; font-weight: 400; color: var(--text-secondary); }

@media (min-width: 640px) {
    /* Four across once there is room for the words. The grid is the mobile fallback, not the design. */
    .size-grid { grid-template-columns: repeat(4, 1fr); }
}

/* One past or upcoming job on the customer's screen. The date and the total are the two things
   somebody scans for, so they anchor opposite ends of the row and the service names take whatever is
   left in the middle. */
/* =================================================================================================
   The price list
   =================================================================================================
   THE ONLY SCREEN IN THIS APPLICATION THAT IS A TABLE. Everywhere else is a column of things read
   one at a time, and a reading-width column is right for that. This is four price columns whose
   entire purpose is being compared against each other - down a column to see how a service scales
   with vehicle size, and across a row to see what a customer of a given size pays for each. Bunching
   them into the left third of a wide screen makes both comparisons harder and wastes the rest.

   It is still a stack of cards on a phone. The grid below only engages once there is room for the
   columns to be wider than the numbers in them; before that, four columns of £1,000.00 would either
   wrap or overflow, and a wrapped price table is worse than no table. */

/* On a phone: the name and description take a full line, and the four bands wrap onto the next one
   as a row. Wrapping rather than a nested container, because the same elements have to become grid
   cells on a wide screen and a wrapper in between would break the grid. */
.price-row {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--space-2) var(--space-4);
    min-height: var(--touch-target);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--border);
    color: inherit;
    text-decoration: none;
}

.price-row:last-of-type { border-bottom: none; padding-bottom: 0; }

/* The column headings only mean anything once the columns exist. */
.price-row--head { display: none; }

.price-row__main { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 0 100%; }

.price-row__name { font-weight: 600; display: flex; align-items: center; gap: var(--space-2); }

/* Two lines at most. It is what the customer is being sold, so it is worth showing - but this is a
   price list, and a paragraph per row turns four comparable numbers into a wall of text. */
.price-row__description {
    font-size: 0.8125rem;
    color: var(--text-secondary);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.price-row__quoted { font-size: 0.8125rem; color: var(--text-secondary); flex: 1 0 100%; }

/* On a phone the bands sit in one wrapping row under the name, each carrying its own label - a
   heading eight rows up is no use once it has scrolled away. */
.price-row__band { display: flex; flex-direction: column; gap: 1px; }

.price-row:not(.price-row--head) .price-row__band { flex: 0 0 auto; }

.price-band__label {
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    color: var(--text-tertiary);
}

/* Tabular figures so the columns line up down the page. Proportional digits make a column of prices
   look ragged, and ragged numbers are harder to compare at a glance - which is the one job here. */
.price-band__value { font-size: 0.9375rem; font-variant-numeric: tabular-nums; }

.price-band__net { font-size: 0.75rem; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }

/* Under the price rather than in a column of its own, because how long a job takes now depends on
   the size of the vehicle - it is a fact about this band, not about the service. */
.price-band__time { font-size: 0.6875rem; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }

/* Hidden on a phone. Duration is useful when laying out a day and is not what somebody opens a price
   list to find out, so it is the first thing to lose when space is short. */
.price-row__time { display: none; }

.price-row__chevron { display: none; }

/* Still legible, still editable, but obviously not on offer. Greying it out entirely would make the
   one screen where somebody turns a service back on the hardest one to read. */
.price-row--off .price-row__name,
.price-row--off .price-band__value { opacity: 0.55; }

@media (min-width: 900px) {
    .price-row {
        display: grid;
        /* The name takes whatever is left. The price columns are fixed so they line up between the
           two cards on the page - packages and add-ons - which a percentage would not do, because
           the two lists have different longest names. */
        grid-template-columns: minmax(0, 1fr) repeat(4, 7rem) 4rem 1.25rem;
        align-items: center;
        gap: var(--space-3);
        padding: var(--space-3) var(--space-2);
        margin: 0 calc(-1 * var(--space-2));
        border-radius: var(--radius-sm);
    }

    .price-row__main,
    .price-row__quoted { flex: initial; }

    .price-row--head {
        display: grid;
        padding-top: 0;
        padding-bottom: var(--space-2);
        border-bottom: 1px solid var(--border-strong);
    }

    .price-row--head .price-row__band,
    .price-row--head .price-row__time {
        font-size: 0.6875rem;
        font-weight: 600;
        letter-spacing: 0.06em;
        text-transform: uppercase;
        color: var(--text-tertiary);
        display: block;
    }

    /* The whole row is the target, so it lights up as one thing rather than the text underlining
       itself while the numbers beside it stay inert. */
    .price-row:not(.price-row--head):hover { background-color: var(--grey-50); }

    /* Per-price labels are redundant once the columns are headed. */
    .price-row:not(.price-row--head) .price-band__label { display: none; }

    .price-row__band { align-items: flex-end; text-align: right; }

    .price-band__value { font-size: 1rem; }

    /* One cell across all four price columns, so it reads as a sentence rather than being squeezed
       into the width of a single figure. */
    .price-row--quoted .price-row__quoted { grid-column: 2 / 6; text-align: right; }

    .price-row__time {
        display: block;
        text-align: right;
        font-size: 0.8125rem;
        color: var(--text-secondary);
        font-variant-numeric: tabular-nums;
    }

    .price-row__chevron { display: block; color: var(--text-tertiary); }

    /* The card's own padding would otherwise cut the hover highlight short of the numbers. */
    .price-table { padding-left: var(--space-5); padding-right: var(--space-5); }
}

/* The currency symbol sits outside the box rather than inside the value, so nobody has to decide
   whether to type it and the number that comes back is always just a number. */
.money-input { display: flex; align-items: center; gap: var(--space-2); }

.money-input__symbol { font-weight: 600; color: var(--text-secondary); flex: 0 0 auto; }

/* One size band on the edit screen: what it costs and how long it takes, side by side. Together
   rather than in two separate lists, so somebody who updates the price of an extra large van has the
   time for the same van in front of them and notices if it is still the small one. */
.band-fields {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    padding: var(--space-3) 0;
    border-top: 1px solid var(--border);
}

.band-fields__name {
    flex: 1 0 100%;
    font-weight: 600;
    font-size: 0.875rem;
}

.band-fields .field { flex: 1 1 9rem; }

/* =================================================================================================
   Two prices, one glance
   =================================================================================================
   Every price appears twice wherever VAT applies: what the customer pays, and the figure before VAT.
   Whoever is on the telephone needs whichever one was asked for without doing arithmetic, and
   reading out the wrong one is a conversation nobody wants to have afterwards.

   The gross figure is the larger and the darker of the two, because it is what a private customer is
   quoted and it is the more common answer. The net one is deliberately quieter rather than hidden -
   a trade customer who reclaims VAT asks for it by name. */

.service-card__price { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; }

.service-card__gross { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }

.service-card__net {
    font-size: 0.75rem;
    font-weight: 400;
    color: var(--text-tertiary);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.price-band__net {
    font-size: 0.75rem;
    color: var(--text-tertiary);
    font-variant-numeric: tabular-nums;
}

.job-row__net { font-size: 0.75rem; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }

/* The booking total, itemised. A single number would hide the split at the exact moment somebody is
   about to say it out loud, which is the one moment the split matters. */
.total-panel {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-4) 0 0;
    border-top: 1px solid var(--border);
}

.total-panel__row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); }

.total-panel__label { color: var(--text-secondary); font-size: 0.875rem; }

.total-panel__value { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* The figure the customer is actually told. Bigger than the working above it, because that is the
   one somebody reads off the screen. */
.total-panel__row--headline { padding-top: var(--space-2); border-top: 1px solid var(--border); }
.total-panel__row--headline .total-panel__label { color: var(--text-primary); font-weight: 600; font-size: 1rem; }
.total-panel__row--headline .total-panel__value { font-weight: 700; font-size: 1.125rem; }

.total-panel__note { font-size: 0.8125rem; color: var(--text-secondary); margin: 0; }

/* The quoted total, pinned in the save bar.

   It sits here rather than only in the form because the service list is long, and the figure somebody
   is about to read out over the telephone must not be the thing that scrolled off the screen. Gross
   is the large figure; the net one beside it is what a VAT-registered trade customer asks for. */
.form-actions__total {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    padding-bottom: var(--space-3);
}

.form-actions__total-label { font-size: 0.8125rem; color: var(--text-secondary); }

.form-actions__total-value {
    font-size: 1.25rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    margin-left: auto;
}

.form-actions__total-net { font-size: 0.75rem; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }

/* =================================================================================================
   The customer screen
   =================================================================================================
   Two columns on a wide screen, one on a phone, split by what each side is FOR rather than by size.

   The left is everything needed to reach somebody or get to them: read once, acted on, and then done
   with. The right is what they own and what has been done to it - the part that grows over years and
   is the reason anybody opens the screen a second time.

   THE MARKUP ORDER IS THE PHONE ORDER, and that is the one that matters: contact, then where they
   are, then the car, then the history. Standing on a driveway that is the order the questions
   arrive in, and no amount of desktop tidiness is worth reshuffling it. */

.detail-grid { display: flex; flex-direction: column; gap: var(--space-4); }

.detail-grid__side,
.detail-grid__main { display: flex; flex-direction: column; gap: var(--space-4); }

/* Label above value in the narrow column, rather than beside it.

   A fixed label column costs 5rem of an already narrow card, and what is left is not enough for an
   email address - "service@colchestermotors.example" broke mid-word across two lines, which reads as
   a rendering fault rather than as an address. Stacking gives the value the full width. */
.detail-grid__side .field-row { flex-direction: column; align-items: flex-start; gap: 1px; }

.detail-grid__side .field-row__label { flex: 0 0 auto; font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; }

@media (min-width: 900px) {
    .detail-grid {
        display: grid;
        /* The reference column is fixed and the history column takes the rest. A percentage split
           would make the contact card grow with the screen for no reason - a telephone number is the
           same width on a laptop as on a phone. */
        grid-template-columns: minmax(0, 21rem) minmax(0, 1fr);
        align-items: start;
        gap: var(--space-4);
    }
}

/* --- The strip under the name ----------------------------------------------------------------- */

/* The four things somebody wants before they have read anything else. Without them the top of the
   screen was a name and two buttons, and every one of these questions meant reading the work list
   and totalling it by eye. */
.stat-strip {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-3);
    margin: 0 0 var(--space-4);
    padding: var(--space-4);
    background-color: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.stat { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.stat__label {
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}

/* Tabular figures so a column of money and dates lines up rather than shimmering as the values
   change between customers. */
.stat__value {
    margin: 0;
    font-size: 1.0625rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

/*
    The qualifier under a figure - "1 booked", "not seen in 10 weeks".

    A BLOCK, and that is the fix rather than the styling. Inline it ran straight on from the value
    above it and rendered "Thu 10 Sept1 booked", which reads as a date nobody can parse. Its own line
    also lets the value keep the size it needs to be readable at arm's length.
*/
.stat__aside {
    display: block;
    font-size: 0.75rem;
    font-weight: 500;
    color: var(--text-tertiary);
}

/* A stat that goes somewhere. It has to look tappable without becoming a button - it is still a
   figure first, and a link second. */
.stat--link { text-decoration: none; color: inherit; cursor: pointer; }

.stat--link:hover .stat__value { text-decoration: underline; }

@media (min-width: 640px) {
    .stat-strip { grid-template-columns: repeat(4, minmax(0, 1fr)); }

    /* Full width is right for a thumb and wrong for a mouse: a Call button stretched across a laptop
       promises something it is not going to be used for nearly as often. */
    .actions-row { display: flex; justify-content: flex-start; }
    .actions-row .button { flex: 0 0 auto; min-width: 9rem; }
}

/* --- A named person at a business ------------------------------------------------------------- */

/* A row rather than a label-and-value pair. The old layout put the job title in the label column and
   the name in the value column, which read as though the title were the more important of the two -
   and left nowhere for the direct number, which is the entire reason for recording the person. */
.person {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--border);
}

.person:last-child { border-bottom: none; padding-bottom: 0; }

.person__body { display: flex; flex-direction: column; gap: 1px; }

.person__name { font-weight: 600; display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

.person__role { font-size: 0.8125rem; color: var(--text-secondary); }

.person__links { display: flex; flex-direction: column; gap: var(--space-1); }

/* Tappable at full row width, because these are dialled from a phone with one thumb. */
.person__link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 2rem;
    font-size: 0.875rem;
    color: var(--brand-accent);
    text-decoration: none;
    word-break: break-word;
}

.person__link:hover { text-decoration: underline; }

/* --- Notes ------------------------------------------------------------------------------------ */

/* Line breaks preserved. Notes are typed as a list far more often than as a paragraph - "gate code
   1234", "prefers mornings", "do not use the front tap" - and collapsing them into prose runs three
   separate facts into one sentence. */
.notes { margin: 0; white-space: pre-wrap; }

/* --- When they were last seen ----------------------------------------------------------------- */

/* Hidden on a phone, where the row is already carrying a name, a plate, a number and a postcode.
   Something has to give at 375px, and this is the least urgent of them - a valeter looking somebody
   up on a driveway is looking for the right person, not auditing when they were last done. */
.customer-card__visit { display: none; }

@media (min-width: 720px) {
    .customer-card { grid-template-columns: auto 1fr auto auto; }

    .customer-card__visit {
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        gap: 1px;
        padding-right: var(--space-2);
        text-align: right;
        white-space: nowrap;
    }

    .customer-card__visit-label {
        font-size: 0.6875rem;
        font-weight: 600;
        letter-spacing: 0.06em;
        text-transform: uppercase;
        color: var(--text-tertiary);
    }

    .customer-card__visit-date { font-size: 0.875rem; font-variant-numeric: tabular-nums; }
}

/* =================================================================================================
   The application shell
   =================================================================================================
   A navigation rail on a desktop, the top bar on a phone. THE SAME LINKS EITHER WAY - one navigation
   rendered twice, not two navigations.

   The rail is the single change that makes this read as an application rather than a web page. Two
   links in the corner of a top bar is what a website does; a persistent rail is what every tool
   somebody already uses all day does, and it gives the screens that do not exist yet - the day view,
   the diary, invoices - somewhere to arrive without another redesign.

   It appears at 1024px, not at the usual 900. Below that a 15rem rail is a sixth of the screen spent
   on four words, and the top bar carries the same links for nothing. */

/* Hidden until there is room for it, and declared BEFORE the media query that turns it on.

   This was the other way round to begin with, which meant the base rule won at equal specificity and
   a desktop got no navigation at all: the rail hidden by this, and the top bar hidden by the query.
   Both rules were individually correct and the order made them cancel out. */
.rail { display: none; }

@media (min-width: 1024px) {
    /*
        Two columns: the rail, and everything else. The environment banner stays full width above
        both, because it has to be impossible to miss.

        SCOPED TO --railed, not to .app-shell. It was on the shell itself, which meant the signed-out
        landing page - which has no rail - had its content squeezed into the column reserved for one.
    */
    .app-shell--railed {
        display: grid;
        grid-template-columns: 15rem minmax(0, 1fr);
        grid-template-rows: auto 1fr;
        min-height: 100vh;
    }

    .app-shell--railed .environment-banner { grid-column: 1 / -1; }

    /* The bar and the rail are the same navigation. Exactly one of them is ever on screen. */
    .topbar { display: none; }

    .rail {
        display: flex;
        flex-direction: column;
        gap: var(--space-5);
        padding: var(--space-4) var(--space-3);
        background-color: var(--brand-primary);
        color: var(--text-on-dark);
        /* Sticky rather than fixed: fixed would take it out of the grid and leave the main column
           to work out its own left margin, which is the sort of thing that drifts by a pixel. */
        position: sticky;
        top: 0;
        align-self: start;
        height: 100vh;
    }

    .app-main { min-width: 0; }
}

.rail__brand {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2);
    color: inherit;
    text-decoration: none;
    min-height: var(--touch-target);
}

.rail__logo { height: 1.75rem; width: auto; flex: 0 0 auto; }

/* WHICH COMPANY, always visible. One person can hold accounts at two valeting companies, and
   realising you are looking at the wrong one by recognising a customer's name is far too late. */
.rail__tenant { font-weight: 600; font-size: 0.9375rem; min-width: 0; overflow: hidden; text-overflow: ellipsis; }

.rail__links { display: flex; flex-direction: column; gap: 2px; }

.rail__link {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-height: var(--touch-target);
    padding: 0 var(--space-3);
    border-radius: var(--radius);
    color: inherit;
    text-decoration: none;
    font-size: 0.9375rem;
    opacity: 0.75;
    transition: background-color 0.12s ease, opacity 0.12s ease;
}

.rail__link:hover { opacity: 1; background-color: rgba(255, 255, 255, 0.08); }

/* Where you are. Without it the navigation forgets which section it is in the moment somebody opens
   a customer, which is a small thing that makes an application feel like a pile of unrelated pages. */
.rail__link--current { opacity: 1; background-color: rgba(255, 255, 255, 0.14); font-weight: 600; }

.rail__icon { flex: 0 0 auto; }

/* Pinned to the bottom, which is where every application this is trying to feel like puts the person
   who is signed in. */
.rail__account {
    margin-top: auto;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding-top: var(--space-4);
    border-top: 1px solid rgba(255, 255, 255, 0.14);
}

.rail__avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 2rem;
    height: 2rem;
    border-radius: var(--radius-full);
    background-color: rgba(255, 255, 255, 0.14);
    font-size: 0.75rem;
    font-weight: 700;
}

.rail__name { font-size: 0.875rem; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.rail__signout {
    margin-left: auto;
    background: none;
    border: none;
    padding: var(--space-2);
    color: inherit;
    opacity: 0.7;
    font-size: 0.75rem;
    cursor: pointer;
    border-radius: var(--radius-sm);
}

.rail__signout:hover { opacity: 1; background-color: rgba(255, 255, 255, 0.08); }

/* =================================================================================================
   The customer card
   =================================================================================================
   A grid rather than a line of text, so the plates start at the same place on every row whether or
   not there is a business badge above them. That was the actual cause of the misalignment: the badge
   sat inside the same flowing line and shoved everything after it along.

   A row per vehicle, because a garage with four vans is the case this has to handle. It used to show
   one - whichever was newest - and say nothing about the rest, so the row looked complete while
   being wrong. */

.customer-card {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    grid-template-areas:
        "avatar name    visit chevron"
        "avatar contact visit chevron"
        "avatar cars    cars  chevron";
    align-items: center;
    column-gap: var(--space-3);
    row-gap: var(--space-1);
    padding: var(--space-4);
    background-color: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-1);
    color: inherit;
    text-decoration: none;
    transition: border-color 0.12s ease, box-shadow 0.12s ease, transform 0.06s ease;
}

.customer-card > .avatar { grid-area: avatar; align-self: start; }
.customer-card__name { grid-area: name; }
.customer-card__contact { grid-area: contact; }
.customer-card__visit { grid-area: visit; align-self: start; }
.customer-card__chevron { grid-area: chevron; }
.customer-card__vehicles { grid-area: cars; }

.customer-card__name {
    font-size: 1rem;
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
}

.customer-card__contact {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    min-width: 0;
    font-size: 0.875rem;
    color: var(--text-secondary);
}

/* The town goes first when there is not room for both. A telephone number is what somebody is
   looking for; where they live is how they tell two of the same name apart. */
.customer-card__where { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.customer-card__vehicles {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    margin-top: var(--space-2);
}

/* Fixed plate column, which is the whole point: every plate in the list lines up down the page,
   whatever is above or beside it.

   Flex rather than a grid, because the row sits inside a grid area that is as wide as the card. A
   1fr name column pushed the size to the far right, where it read as a separate piece of information
   about the customer rather than as a fact about the car beside it. Everything clusters left and the
   slack goes to the end. */
.vehicle-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-height: 1.75rem;
}

.vehicle-row__plate { flex: 0 0 6.5rem; display: flex; }

.vehicle-row__name { flex: 0 1 auto; min-width: 0; font-size: 0.875rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.vehicle-row__size { flex: 0 0 auto; font-size: 0.75rem; color: var(--text-tertiary); white-space: nowrap; }

/* Which van the plate search actually hit. With one vehicle per row this was implicit; with four it
   is a real question. */
.vehicle-row--matched .vehicle-row__name { font-weight: 600; }

/* A vehicle recorded before anybody has seen it. Saying so beats a gap where a plate should be. */
.plate--unknown {
    background-color: var(--grey-100);
    border-color: var(--border);
    color: var(--text-tertiary);
    font-weight: 500;
    letter-spacing: 0;
}

/* Four across once there is room, rather than the two the customer screen uses. */
.stat-strip--wide { margin-bottom: var(--space-4); }

@media (min-width: 900px) {
    .stat-strip--wide { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

@media (max-width: 719px) {
    /* On a phone the visit column goes, and the card becomes three stacked areas. There is not room
       for a name, a number, a postcode, a plate and a date, and the date is the least urgent of
       them - somebody looking a customer up on a driveway wants the right person, not an audit. */
    .customer-card {
        grid-template-columns: auto minmax(0, 1fr) auto;
        grid-template-areas:
            "avatar name    chevron"
            "avatar contact chevron"
            "cars   cars    cars";
    }

    .customer-card__vehicles { margin-top: var(--space-3); }

    .vehicle-row__size { display: none; }
}

/* =================================================================================================
   The team
   =================================================================================================
   The same shape as the price list: a stacked card on a phone, aligned columns once there is room.
   A list of people is compared down a column - who has not signed in, who is still an owner - in
   exactly the way a list of prices is. */

.person-row {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-height: var(--touch-target);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--border);
    color: inherit;
    text-decoration: none;
}

.person-row:last-of-type { border-bottom: none; padding-bottom: 0; }

.person-row--head { display: none; }

.person-row__who { display: flex; flex-direction: column; gap: 1px; min-width: 0; }

.person-row__name { font-weight: 600; display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

.person-row__email { font-size: 0.8125rem; color: var(--text-secondary); word-break: break-word; }

.person-row__role { font-size: 0.875rem; }

.person-row__seen { font-size: 0.875rem; color: var(--text-secondary); font-variant-numeric: tabular-nums; }

/* An account created and never used is either somebody who needs help or a login that should not
   exist. Worth a second glance rather than blending into a column of dates. */
.person-row__never { color: var(--text-tertiary); font-style: italic; }

/* Still legible, still editable, but obviously not able to sign in. Greying it out entirely would
   make the one screen where somebody is switched back on the hardest one to read. */
.person-row--off .person-row__name,
.person-row--off .person-row__role { opacity: 0.55; }

@media (min-width: 900px) {
    .person-row {
        display: grid;
        grid-template-columns: minmax(0, 1fr) 9rem 9rem 1.25rem;
        align-items: center;
        gap: var(--space-3);
        padding: var(--space-3) var(--space-2);
        margin: 0 calc(-1 * var(--space-2));
        border-radius: var(--radius-sm);
    }

    .person-row--head {
        display: grid;
        padding-top: 0;
        padding-bottom: var(--space-2);
        border-bottom: 1px solid var(--border-strong);
        font-size: 0.6875rem;
        font-weight: 600;
        letter-spacing: 0.06em;
        text-transform: uppercase;
        color: var(--text-tertiary);
    }

    .person-row:not(.person-row--head):hover { background-color: var(--grey-50); }
}

/* =================================================================================================
   Dashboard, the day, and booking
   =================================================================================================
   Mobile first throughout: every rule here is the phone layout, and the media queries at the bottom
   widen it. Doing it the other way round means the smallest screen carries the most overrides, which
   is backwards for a product used on a driveway 99% of the time.
*/

/*
    THE SAME CONTAINER EVERY OTHER SCREEN USES, which this was missing entirely.

    Without a max-width it ran the full width of the monitor: five action cards stretched across two
    thousand pixels, and a job row with the customer at the far left and the price at the far right
    with a hand-span of nothing between them. Wider than the 46rem reading width because this screen
    is cards and figures rather than prose, but bounded - past about 68rem the eye stops being able
    to associate the left of a row with the right of it.
*/
.dashboard {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
    width: 100%;
    max-width: 68rem;
    margin: 0 auto;
    padding: var(--space-5) max(var(--space-4), env(safe-area-inset-right))
             calc(var(--space-8) + env(safe-area-inset-bottom)) max(var(--space-4), env(safe-area-inset-left));
}

.dashboard__header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); }

.dashboard__greeting {
    margin: 0 0 2px;
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}

/* --- Quick actions ----------------------------------------------------------------------------- */

/*
    Two columns on a phone, with the primary spanning both.

    A single column pushes the day's work below the fold, and the day is what the screen is for. Two
    columns keeps every action within reach of a thumb while leaving the round visible underneath.
*/
.quick-actions {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-2);
}

.quick-action {
    display: flex;
    flex-direction: column;
    gap: 2px;
    /* Comfortably past the 44px minimum: this is tapped with a thumb, frequently in gloves. */
    min-height: var(--touch-target);
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background-color: var(--surface);
    color: var(--text-primary);
    text-decoration: none;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.quick-action:hover { border-color: var(--border-strong); box-shadow: var(--shadow-1); }

.quick-action:active { transform: translateY(1px); }

/* Spans the row and inverts, because booking is the commonest thing anybody does here and the whole
   reason this screen exists is that it used to be buried inside the customer edit form. */
.quick-action--primary {
    grid-column: 1 / -1;
    background-color: var(--brand-primary);
    border-color: var(--brand-primary);
    color: var(--text-on-dark);
}

.quick-action--primary:hover { background-color: #000; border-color: #000; }

.quick-action__icon { width: 1.25rem; height: 1.25rem; margin-bottom: var(--space-1); }

.quick-action__label { font-weight: 600; font-size: 0.9375rem; }

.quick-action__hint { font-size: 0.8125rem; opacity: 0.75; }

/* --- The day --------------------------------------------------------------------------------- */

.day { display: flex; flex-direction: column; gap: var(--space-3); }

.day__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    flex-wrap: wrap;
}

/* The stepper, kept together on the right so the buttons never move between one day and the next. */
.day__nav { display: flex; align-items: center; gap: var(--space-2); flex: 0 0 auto; }

.day__step {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Full touch target. Stepping a day is done one-handed while holding something else. */
    width: var(--touch-target);
    height: var(--touch-target);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background-color: var(--surface);
    color: var(--text-secondary);
    cursor: pointer;
}

.day__step:hover { border-color: var(--border-strong); color: var(--text-primary); }

.day__title { min-width: 0; flex: 1 1 auto; }

.day__date { margin: 0; font-size: 1.25rem; font-weight: 680; letter-spacing: -0.01em; }

.day__meta { margin: 0; font-size: 0.8125rem; color: var(--text-secondary); }

.day__empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-6) var(--space-4);
    border: 1px dashed var(--border);
    border-radius: var(--radius-lg);
    color: var(--text-secondary);
    text-align: center;
}

.day__empty p { margin: 0; }

/* --- One job on the day ------------------------------------------------------------------------ */

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

/*
    A grid rather than stacked text.

    The time and the plate are what the eye goes to when checking "is this the one I am standing in
    front of", so they get their own columns and stay in the same place down the whole list. Prose
    that reflows puts them somewhere different on every row.
*/
.job-card {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: "when body side";
    gap: var(--space-3);
    align-items: start;
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-left: 3px solid var(--brand-accent);
    border-radius: var(--radius);
    background-color: var(--surface);
    position: relative;
}

/* Done and cancelled recede rather than disappearing. A completed job still answers "did we do that
   one", and removing it from the day would make the list disagree with itself as the day goes on. */
.job-card--done { border-left-color: #15803d; }

.job-card--done .job-card__what,
.job-card--done .job-card__where { color: var(--text-tertiary); }

.job-card--cancelled { border-left-color: var(--grey-300); opacity: 0.6; }

.job-card--cancelled .job-card__who { text-decoration: line-through; }

.job-card__when { grid-area: when; display: flex; flex-direction: column; gap: 2px; }

.job-card__time { font-weight: 700; font-size: 0.9375rem; font-feature-settings: "tnum"; }

.job-card__duration { font-size: 0.75rem; color: var(--text-tertiary); font-feature-settings: "tnum"; }

.job-card__body { grid-area: body; display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.job-card__who { font-weight: 600; color: var(--text-primary); text-decoration: none; }

.job-card__who:hover { text-decoration: underline; }

.job-card__what { font-size: 0.875rem; color: var(--text-secondary); }

.job-card__where { font-size: 0.8125rem; color: var(--text-tertiary); }

/* Called out rather than blended in. "Round the back, gate code 1234" is the line that saves a
   telephone call, and it is worthless if it reads as more address. */
.job-card__access {
    margin-top: var(--space-1);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-sm);
    background-color: color-mix(in srgb, #b45309 8%, white);
    color: #b45309;
    font-size: 0.8125rem;
}

.job-card__side {
    grid-area: side;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 2px;
    text-align: right;
}

.job-card__vehicle { font-size: 0.75rem; color: var(--text-tertiary); }

.job-card__total { font-weight: 700; font-feature-settings: "tnum"; }

.job-card__novat { font-size: 0.6875rem; color: var(--text-tertiary); }

/*
    The call button, bottom right of the card.

    Positioned rather than in the flow so it keeps a full touch target without pushing the text
    around, and a tel: link works with no signal because the dialler is on the phone.
*/
.job-card__call {
    position: absolute;
    right: var(--space-3);
    bottom: var(--space-3);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* The full touch target, not a smaller circle that happens to fit the icon. This is pressed with
       a thumb, outdoors, frequently in gloves, and it dials a customer - a mis-tap either does
       nothing or rings the wrong person. */
    width: var(--touch-target);
    height: var(--touch-target);
    border: 1px solid var(--border);
    border-radius: var(--radius-full);
    background-color: var(--surface);
    color: var(--text-secondary);
}

.job-card__call:hover { border-color: var(--border-strong); color: var(--text-primary); }

/* --- Booking ----------------------------------------------------------------------------------- */

/* Matched to the search box above it, because they are one control. Results wider than the box they
   came from read as a separate list that happens to be nearby. */
.picker-list { display: flex; flex-direction: column; gap: var(--space-1); margin-top: var(--space-2); max-width: 34rem; }

.picker-list__item {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-height: var(--touch-target);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background-color: var(--surface);
    text-align: left;
    cursor: pointer;
}

.picker-list__item:hover { border-color: var(--border-strong); background-color: var(--grey-50); }

.picker-list__name { font-weight: 600; }

.picker-list__detail { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; font-size: 0.8125rem; color: var(--text-secondary); }

/* Quieter than the street beside it, and in tabular figures so a column of them lines up. The street
   is what somebody scans; the postcode is what they read back to confirm. */
.picker-list__postcode { color: var(--text-tertiary); font-variant-numeric: tabular-nums; }

/* Whoever has already been chosen, as a line rather than a form. Coming from a customer's record
   three of the four answers are known, and re-presenting them as editable fields invites somebody to
   change a name when they meant to book a wash. */
.chosen {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background-color: var(--grey-50);
}

.chosen__name { font-weight: 600; }

.chosen__detail { font-size: 0.8125rem; color: var(--text-secondary); flex: 1 1 auto; min-width: 0; }

/*
    auto-FILL rather than auto-FIT.

    auto-fit collapses the empty tracks and stretches whatever is there across the whole row, so a
    customer with one car got a single card the width of the form - a large empty box asking a
    question with one answer. auto-fill keeps the track width, so one card looks like one card and
    three sit in a row.
*/
.vehicle-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); gap: var(--space-2); }

.vehicle-option {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-1);
    min-height: var(--touch-target);
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background-color: var(--surface);
    text-align: left;
    cursor: pointer;
}

.vehicle-option:hover { border-color: var(--border-strong); }

/* Two signals, not one. A border alone is easy to miss on a bright screen outdoors, and colour alone
   fails for anybody who cannot distinguish it. */
.vehicle-option--on {
    border-color: var(--brand-primary);
    box-shadow: inset 0 0 0 1px var(--brand-primary);
    background-color: var(--grey-50);
}

.vehicle-option__name { font-weight: 600; font-size: 0.875rem; }

.vehicle-option__size { font-size: 0.75rem; color: var(--text-tertiary); }

/* --- What is being charged --------------------------------------------------------------------- */

.charges { display: flex; flex-direction: column; gap: var(--space-2); }

/*
    A row, not a two-row grid.

    It was a grid with the box spanning both rows, which meant the name was centred in the FIRST row
    rather than against the box - so it floated visibly above the figure it belongs to, and by more
    when there was no note under it. A flex row centres the whole left-hand block against the box,
    which is what the eye expects.
*/
.charge {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background-color: var(--surface);
}

.charge__what { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.charge__name { font-weight: 600; font-size: 0.9375rem; }

/* What the list said, when it is no longer what is being charged. A fact, not a telling-off:
   charging something else is the whole point of this section. */
.charge__list { font-size: 0.75rem; color: var(--text-tertiary); }

.charge__amount { display: flex; align-items: center; gap: var(--space-1); flex: 0 0 auto; }

.charge__currency { color: var(--text-tertiary); font-weight: 600; }

/*
    Wide enough for four figures and pence without being a full-width field.

    16px, like every other input here, because anything smaller makes iOS zoom the whole page in when
    it takes focus - and it never zooms back out, so the rest of the form is left half off screen.
*/
.charge__input {
    width: 6.5rem;
    min-height: var(--touch-target);
    font-size: 1rem;
    font-weight: 600;
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/*
    The date, sized to a date rather than to the form.

    A full-width box for eight characters reads as though something longer is expected, and on a
    phone it puts the native picker's chrome miles from the value it belongs to.
*/
.field--date .input, .fieldset .input[type="date"] { max-width: 12rem; }

/* What is already on the chosen day. Deliberately quiet: it is context for a decision, not a list to
   work from, and anything louder competes with the booking being made. */
.day-peek { margin: var(--space-2) 0 var(--space-1); padding: 0; list-style: none; display: flex; flex-direction: column; gap: 2px; }

.day-peek__item {
    display: flex;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-sm);
    background-color: var(--grey-50);
    font-size: 0.8125rem;
}

.day-peek__when { font-feature-settings: "tnum"; color: var(--text-tertiary); flex: 0 0 auto; }

.day-peek__who { font-weight: 600; flex: 0 0 auto; }

.day-peek__what { color: var(--text-secondary); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/*
    The total and the commit button, stuck to the bottom of the viewport.

    A figure that scrolls away is a figure somebody reads out wrong over the telephone. Sticky keeps
    "what does that come to" answerable without scrolling back through the services.
*/
.form-footer {
    position: sticky;
    bottom: 0;
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin: var(--space-4) calc(-1 * var(--space-4)) 0;
    padding: var(--space-3) var(--space-4);
    /* Opaque, not translucent. Text scrolling underneath a frosted bar is unreadable in sunlight. */
    background-color: var(--surface);
    border-top: 1px solid var(--border);
}

.form-footer__total { display: flex; flex-direction: column; gap: 0; flex: 0 0 auto; }

.form-footer__gross { font-size: 1.25rem; font-weight: 700; font-feature-settings: "tnum"; }

.form-footer__net { font-size: 0.75rem; color: var(--text-tertiary); font-feature-settings: "tnum"; }

.form-footer .button { flex: 1 1 auto; }

/* --- Wider screens ----------------------------------------------------------------------------- */

@media (min-width: 48rem) {
    .quick-actions { grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); }

    /* The primary stops spanning once there is room for everything on one row. */
    .quick-action--primary { grid-column: auto; }

    /*
        A fourth column for the call button, rather than letting it fall wherever the grid puts it.

        Unplaced it auto-flowed into the second row's first cell - orphaned under the time with the
        whole row empty beside it, which is exactly what it looked like. Naming the area is the fix;
        making it static without one was the bug.
    */
    .job-card {
        grid-template-columns: auto minmax(0, 1fr) auto auto;
        grid-template-areas: "when body side call";
        gap: var(--space-4);
        padding: var(--space-4);
        align-items: center;
    }

    .job-card__call { grid-area: call; position: static; align-self: center; }
}

/* =================================================================================================
   One job
   ================================================================================================= */

/*
    The card opens the job, laid under its contents rather than wrapped around them.

    Wrapping the card in a link would swallow the customer link and the call button inside it, and a
    card where every tap goes to the same place is a card that cannot ring anybody. Stretched behind
    the content, the specific controls sit on top and win.
*/
.job-card__open {
    position: absolute;
    inset: 0;
    z-index: 0;
    border-radius: inherit;
}

.job-card__body,
.job-card__side,
.job-card__when,
.job-card__call { position: relative; z-index: 1; }

.job-card:hover { border-color: var(--border-strong); }

/* --- The job's own screen ---------------------------------------------------------------------- */

.job-detail {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-4);
    justify-content: space-between;
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background-color: var(--surface);
}

.job-detail__vehicle { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-1); }

.job-detail__car { font-weight: 600; }

.job-detail__size { font-size: 0.8125rem; color: var(--text-tertiary); }

.job-detail__where { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-2); min-width: 0; }

.job-detail__address { color: var(--text-secondary); }

/* --- What was booked --------------------------------------------------------------------------- */

.job-lines { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; }

.job-lines__row {
    display: flex;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-2) 0;
    border-bottom: 1px solid var(--border);
}

/* Tabular figures so the column of money lines up rather than shimmering down the page. */
.job-lines__price { font-variant-numeric: tabular-nums; flex: 0 0 auto; }

.job-lines__row--total {
    border-bottom: none;
    padding-top: var(--space-3);
    font-weight: 700;
    border-top: 2px solid var(--border-strong);
}

/* --- What happened ----------------------------------------------------------------------------- */

.summary { margin: 0; display: flex; flex-direction: column; gap: var(--space-2); }

.summary__row {
    display: grid;
    /* A fixed label column, so the answers line up down the list instead of starting wherever the
       longest word happens to end. */
    grid-template-columns: 6rem minmax(0, 1fr);
    gap: var(--space-3);
    align-items: baseline;
}

.summary__row dt { font-size: 0.8125rem; color: var(--text-tertiary); }

.summary__row dd {
    margin: 0;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    min-width: 0;
}

/* =================================================================================================
   A customer's work, as a timeline
   ================================================================================================= */

/*
    THE PLATE STRETCHED ACROSS THE CARD, and this is why.

    A plate is inline-block, but inside a flex COLUMN it is a flex item - and a flex item's default
    alignment is stretch, so it filled the whole width and rendered as a yellow bar rather than a
    number plate. It looked right in the vehicles list beside it, which is a flex row, and wrong in
    the work list, which is a column. Aligning to the start makes it its own width wherever it sits.
*/
.plate { align-self: flex-start; }

.timeline { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; }

/*
    The rail is drawn by the items rather than behind them.

    A single line behind the list has to be told where to stop, and it always stops in the wrong place
    on the last entry - either short of it or running past into nothing. Each item drawing its own
    segment means the last one simply does not draw one.
*/
.timeline__item {
    position: relative;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--space-3);
    padding-bottom: var(--space-4);
}

.timeline__item:last-child { padding-bottom: 0; }

.timeline__item:not(:last-child)::before {
    content: "";
    position: absolute;
    /* From under the marker to the top of the next one. */
    left: 0.3125rem;
    top: 1.1rem;
    bottom: 0;
    width: 1px;
    background-color: var(--border);
}

.timeline__marker {
    width: 0.6875rem;
    height: 0.6875rem;
    margin-top: 0.375rem;
    border-radius: var(--radius-full);
    /* A ring rather than a dot for anything not yet done, so upcoming work reads as an outline
       waiting to be filled in - which is what it is. */
    border: 2px solid var(--border-strong);
    background-color: var(--surface);
    flex: 0 0 auto;
}

.timeline__item--done .timeline__marker { background-color: #15803d; border-color: #15803d; }

.timeline__item--doing .timeline__marker { background-color: #b45309; border-color: #b45309; }

.timeline__item--upcoming .timeline__marker { border-color: var(--brand-accent); }

.timeline__item--cancelled .timeline__marker { border-color: var(--grey-300); background-color: var(--grey-100); }

.timeline__item--cancelled .timeline__what { text-decoration: line-through; color: var(--text-tertiary); }

/* The whole entry opens the job, which is the natural way in from a customer's record. */
.timeline__entry {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "when money" "what money";
    gap: 2px var(--space-3);
    align-items: start;
    padding: var(--space-1) var(--space-2);
    margin: calc(-1 * var(--space-1)) calc(-1 * var(--space-2));
    border-radius: var(--radius);
    color: inherit;
    text-decoration: none;
}

.timeline__entry:hover { background-color: var(--grey-50); }

.timeline__when {
    grid-area: when;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    font-weight: 600;
    font-size: 0.9375rem;
}

.timeline__what {
    grid-area: what;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    font-size: 0.875rem;
    color: var(--text-secondary);
}

.timeline__money {
    grid-area: money;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 2px;
    flex: 0 0 auto;
}

/* Never shrinks and never wraps: it is the amount somebody was charged, and it must not end up
   reading as part of the service description beside it. */
.timeline__total { font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }

.timeline__net { font-size: 0.75rem; color: var(--text-tertiary); white-space: nowrap; }

/* The vehicle card is a link now, so it needs to look tappable and not inherit link colour. */
.vehicle { color: inherit; text-decoration: none; transition: border-color 0.15s ease, background-color 0.15s ease; }

.vehicle:hover { border-color: var(--border-strong); background-color: var(--grey-50); }

/* Actions under a heading, where a page has one thing worth doing from it. */
.page__actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-4); }
