/*
    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);

    /* Buttons that take somebody to another screen: Edit, Add vehicle, Back to the team. Blue,
       because blue is what everybody already reads as "this goes somewhere", and nothing else in a
       palette of warm greys uses it. */
    --navigate: #1d4ed8;
    --navigate-surface: #eff4ff;

    --danger:  #b42318;
    --danger-surface: #fef3f2;
    --warning: #b54708;
    --warning-surface: #fffaeb;
    /* The green the success badge already used, named so a second use does not invent a third. */
    --success: #15803d;
    --success-surface: #f0fdf4;

    /* 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;
    /*
        How far a card's heading hangs below its top border: half of its 11px type on a line half as
        tall again. A form section's first field starts this far plus the card's padding below the
        border - the browser does that for a legend - and every other card is padded to match.
    */
    --heading-overhang: 0.515625rem;

    --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;
}

/* The actions never shrink and the brand does: on the narrowest phones the company name gives way
   with an ellipsis, rather than the initials being pushed off the edge of the screen. */
.topbar__actions { display: flex; align-items: center; gap: var(--space-2); flex: 0 0 auto; }

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

.topbar__brand .topbar__tenant { min-width: 0; }

/*
    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;
    /* A full touch target around a smaller circle: what is tapped is 48px, what is seen is 36px. */
    width: var(--touch-target);
    height: var(--touch-target);
    margin-right: calc(-1 * var(--space-2));
    padding: 0;
    border: none;
    background: none;
    color: var(--text-on-dark);
    cursor: pointer;
    border-radius: var(--radius-full);
}

.topbar__initials {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: var(--radius-full);
    background-color: rgba(255, 255, 255, 0.14);
    font-size: 0.8125rem;
    font-weight: 700;
    transition: background-color 150ms ease;
}

.topbar__user:hover .topbar__initials,
.topbar__user[aria-expanded="true"] .topbar__initials { background-color: rgba(255, 255, 255, 0.28); }

.topbar__user:focus-visible { outline: 2px solid #ffffff; outline-offset: -2px; }



/* 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); }

/* Roles on the team list. Their own colours rather than the brand's: a company whose brand is near
   black, like Pickett's, would otherwise get an Owner badge nobody could tell from a Staff one. */
.badge--role-owner { background-color: color-mix(in srgb, #1d4ed8 9%, white); color: #1d4ed8; }
.badge--role-administrator { background-color: color-mix(in srgb, #7c3aed 9%, white); color: #6d28d9; }

/* The supplier's own account on a company's team list. Solid rather than tinted, so it can never
   be mistaken for one of the role colours beside it. */
.badge--service-round { background-color: var(--grey-900); color: white; }

/* 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; }
/* Blue is in progress: somebody is on it now. Booked-but-not-started is the plain one, because it
   is the state nearly every job is in and a colour on all of them would say nothing. */
.badge--info { background-color: color-mix(in srgb, #1d4ed8 9%, white); color: #1d4ed8; }
.badge--scheduled { background-color: var(--grey-100); color: var(--text-primary); }

/* 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);
    /* grey-400 rather than the pale border cards use, and a little lift: on a white card the old
       edge was so faint that a button read as a label with a box round it. */
    border-color: var(--grey-400);
    color: var(--text-primary);
    box-shadow: var(--shadow-1);
}

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

/*
    A button that takes somebody to another screen rather than doing something here: Edit, Add
    vehicle, Add booking, Back to customers.

    Its own colour so the two kinds can be told apart before tapping. "Keep it" and "Remove" act on
    the screen in front of you; these leave it. Call and Email are deliberately NOT this - they hand
    over to the phone or the mail app, which is doing something, not going somewhere in this one.
*/
.button--navigate {
    border-color: var(--navigate);
    color: var(--navigate);
}

.button--navigate:hover:not(:disabled) {
    background-color: var(--navigate-surface);
    border-color: var(--navigate);
}

/*
    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); }

/*
    Room at the bottom of any page with the button, so the last line can scroll clear of it.

    The page's usual 64px bottom padding ends exactly where the button's top edge is (16px up plus
    48px tall), so the last line of text sat right under it. The extra 24px is the same gap as
    between cards. Phones only, because the button is hidden on wider screens.
*/
.page:has(.fab),
.page:has(~ .fab) {
    padding-bottom: calc(var(--space-4) + var(--touch-target) + var(--space-5) + env(safe-area-inset-bottom));
}

.desktop-only { display: none; }

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

    .page:has(.fab),
    .page:has(~ .fab) {
        padding-bottom: calc(var(--space-8) + env(safe-area-inset-bottom));
    }
    .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; }

/* The line under the name: a job's day, a vehicle's size and colour, with any status beside it. */
.detail-header__sub {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-top: 2px;
    color: var(--text-secondary);
    font-size: 0.9375rem;
}

/* A vehicle's plate, sitting above its make and model in the pinned bar. */
.detail-header__plate { display: inline-block; margin-bottom: var(--space-2); }

/* Where they heard about us, as the one line in its card. */
.lead-source { margin: 0; }

/*
    The customer's name and the Call and Email buttons, pinned under the top bar while the rest of
    the page scrolls.

    Pinned below the top bar on a phone, and to the very top on a wide screen, where the top bar gives
    way to the side rail. It carries the page's own background so what scrolls underneath is hidden
    rather than showing through, and it reaches out over the page's side padding so there is no gap
    either side for the cards to show in.
*/
.customer-bar {
    position: sticky;
    top: var(--header-height);
    z-index: 10;
    margin: 0 calc(-1 * max(var(--space-4), env(safe-area-inset-left))) var(--space-4);
    padding: var(--space-3) max(var(--space-4), env(safe-area-inset-left)) var(--space-3);
    background-color: var(--surface-sunken);
}

.customer-bar .detail-header { margin: 0 0 var(--space-3); }

.customer-bar .actions-row { margin-bottom: 0; }

/* A shadow under the bar only once something has scrolled beneath it, where the browser can tell -
   at the top of the page it would be a line under nothing. Browsers without scroll-linked
   animations simply never show it. */
@supports (animation-timeline: scroll()) {
    .customer-bar {
        animation: customer-bar-lift linear both;
        animation-timeline: scroll();
        animation-range: 0 3rem;
    }

    @keyframes customer-bar-lift {
        from { box-shadow: none; }
        to { box-shadow: 0 6px 12px -10px rgba(0, 0, 0, 0.35); }
    }
}

@media (min-width: 1024px) {
    .customer-bar { top: 0; }
}

/*
    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);
    /* 24px rather than 12, because a heading now rises 8px above its card's top edge and would
       otherwise sit almost on the card above. */
    margin-bottom: var(--space-5);
}

/* The pencil in a card's corner. The icon is small, the target is not: 48px square, pulled back into
   the corner by negative margins so the heading row is no taller than it was. */
.card__edit {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--touch-target);
    height: var(--touch-target);
    border-radius: var(--radius-full);
    color: var(--text-tertiary);
    flex: 0 0 auto;
}

.card__edit:hover { color: var(--text-primary); background-color: var(--grey-100); }

/* The same pencil as a button, in a panel's top corner, for a panel edited in place. */
.panel__edit {
    position: absolute;
    top: 0;
    right: 0;
    border: none;
    background: none;
    cursor: pointer;
}

/*
    Every card's and panel's heading sits on its top edge, the same as a form section's legend, so a
    record and the form that edits it are drawn the same way.

    A fieldset gets this from the browser; everything else is placed there by hand. Taken out of the
    flow and centred on the border, with a background that is the page above the line and the card
    below it, so the border seems to stop either side of the words - the same cut-out a legend makes.
    Because it no longer takes up a line inside the card, the content starts at the card's own
    padding, exactly as a form section's first field does.
*/
.card,
.panel { position: relative; }

/* Room for the part of the heading that hangs inside, so the content starts the same distance below
   the border as a form section's first field does. */
.card:has(> .card__heading, > .card__top > .card__heading),
.panel:has(> .panel__title) {
    padding-top: calc(var(--space-4) + var(--heading-overhang));
}

.card > .card__heading,
.card > .card__top > .card__heading,
.panel > .panel__title {
    position: absolute;
    /* Half a pixel up, onto the middle of the 1px border rather than just inside it. */
    top: -0.5px;
    left: calc(var(--space-4) - var(--space-2));
    transform: translateY(-50%);
    margin: 0;
    padding: 0 var(--space-2);
    background: linear-gradient(to bottom, var(--surface-sunken) 50%, var(--surface) 50%);
    white-space: nowrap;
}

/*
    A button or pencil in a card's top corner - "Add vehicle", "Add booking", the pencils.

    With the heading up on the border, the corner is free, and the control sits in it the same
    distance from the top edge as from the right - the card's own padding, both ways. A button takes
    a row of its own, with the card's content starting beneath it.
*/
.card__top {
    display: flex;
    justify-content: flex-end;
    /* Back up by the heading's overhang, so the button is the card's padding from the top edge. */
    margin: calc(-1 * var(--heading-overhang)) 0 var(--space-4);
}

/*
    A pencil takes no row: it is pinned in the corner, so a card whose only control is a pencil does
    not grow an empty line at its top. Its 48px target reaches right into the corner, which leaves the
    icon itself the card's padding from the top and the right, like a button's.

    The first thing in the card keeps a little room on its right so a long email address cannot run
    underneath the pencil. Padding rather than margin, so a divider under that first row still runs
    the full width of the card.
*/
/* The row itself steps out of the layout, so the heading inside it is still placed against the card
   rather than against the row. */
.card__top--pencil { display: contents; }

.card__top--pencil > .card__edit {
    position: absolute;
    top: 0;
    right: 0;
    margin: 0;
}

.card__top--pencil + * { padding-right: var(--space-5); }

/*
    ONE card heading, everywhere: the customer screen's cards, every form section, the job screen's
    panels. Same size, same weight, same line height, the same distance from the top of its card and
    the same 16px to whatever is under it.

    They were three rules that had drifted - a form's section heading sat on a taller line with more
    room beneath it than the customer screen's card headings, so the record and the form that edits
    it did not quite line up. The form's version won, being the easier of the two to read.
*/
.card__heading,
.fieldset__legend,
.panel__title {
    font-size: 0.6875rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    line-height: 1.5;
    color: var(--text-tertiary);
}

.card__heading { margin: 0; }

.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;
}

/* Something to put right rather than something that has gone wrong, so amber rather than red. */
.notice--warning {
    margin-top: var(--space-3);
    background-color: var(--warning-surface);
    border-color: color-mix(in srgb, var(--warning) 25%, transparent);
    color: var(--warning);
}

.vehicle {
    /*
        A grid, so every row has the same shape whatever is in it: the plate in a column of its own,
        the name and the colour-and-size stacked beside it, and a chevron saying the row opens. It
        used to be one wrapping line, which kept short names on one line and broke long ones onto
        two - so two cars on the same card were laid out differently.
    */
    display: grid;
    grid-template-columns: 7.5rem minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-3);
    min-height: var(--touch-target);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--grey-100);
    color: inherit;
    text-decoration: none;
}

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

.vehicle--sold { opacity: 0.6; }

.vehicle__plate { display: flex; }

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

/* One line each, cut short with an ellipsis rather than wrapping, which is what keeps every row the
   same height. The full name is on the vehicle's own screen. */
.vehicle__name,
.vehicle__meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.vehicle__name { font-weight: 600; }

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

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

/* =================================================================================================
   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); }

/* Styled with .card__heading; only its spacing is its own. */
.fieldset__legend {
    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)); }

/*
    Every form, laid out as cards.

    Each section is one card, exactly the card the customer screen draws, so a record and the form
    that edits it look like the same thing. Only ONE layer: the grey page, the white section, and the
    fields on it. Anything repeated inside a section - a second number, another person, another
    vehicle - is divided off by a rule rather than given a box of its own, because a card inside a
    card inside the page is where a form stops looking designed and starts looking nested.

    THE FULL WIDTH OF THE PAGE, like every other screen. Forms used to stop at 42rem, which made
    moving from the customer list into the form that edits it look like a different page - the
    cards jumped narrower. The cards take the width; the text boxes inside them are still capped
    (see .page .field > .input), so an email address does not sit in a box a thousand pixels long.
*/
.form--sectioned { gap: var(--space-5); }

.form--sectioned .fieldset {
    background-color: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-1);
    padding: var(--space-4);
    gap: var(--space-4);
}

/* The rule between sections is the gap between cards now, so it goes. */
.form--sectioned .fieldset + .fieldset { margin-top: 0; padding-top: var(--space-4); border-top: 1px solid var(--border); }

/*
    The heading sits ON the card's top edge, cut into the border - which is what a browser does with a
    fieldset's legend by itself. Every other card on the site is drawn to match (see .card__heading).

    Padded either side so the border stops short of the words, and pulled back by the same amount so
    the words still line up with the fields beneath them.
*/
.form--sectioned .fieldset__legend {
    margin: 0 0 0 calc(-1 * var(--space-2));
    padding: 0 var(--space-2);
}

.form--sectioned .fieldset + .fieldset > .fieldset__legend { margin-top: 0; }

/* A section with nothing in it yet - the booking's charges before a service is chosen - is not an
   empty card. It stays in the page, so what it holds is not rebuilt each time, but draws nothing. */
.form--sectioned .fieldset.fieldset--empty { display: none; }

/* Grey panels that already sit inside a section - the chosen customer on the booking screen, the
   password rules - keep their fill but lose their outline. An outlined box inside an outlined card is
   the second layer this layout exists to avoid. */
.form--sectioned .chosen,
.form--sectioned .checklist { border-color: transparent; }

/* Repeated entries: no box of their own, a rule between them. */
.form--sectioned .contact-card {
    border: none;
    border-radius: 0;
    padding: 0;
    background: none;
    gap: var(--space-3);
}

.form--sectioned .contacts { gap: var(--space-4); }

.form--sectioned .contact-card + .contact-card { border-top: 1px solid var(--grey-100); padding-top: var(--space-4); }

/* The numbers and the email addresses in one card, ruled apart like two entries are. */
.entry-group { display: flex; flex-direction: column; gap: var(--space-3); }

.entry-group + .entry-group { border-top: 1px solid var(--grey-100); padding-top: var(--space-4); }

/*
    "Add another number". A dashed outline the full width of the card: plainly a place where
    something else can go, a full thumb's height, and quieter than a real button - it is the thing
    nobody needs on most customers.
*/
.entry-add {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    width: 100%;
    min-height: var(--touch-target);
    padding: var(--space-2) var(--space-4);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius);
    background: none;
    color: var(--text-secondary);
    font: inherit;
    font-size: 0.9375rem;
    font-weight: 500;
    cursor: pointer;
}

.entry-add:hover { color: var(--text-primary); border-color: var(--grey-400); background-color: var(--grey-50); }

/*
    The same dashed row used as a link on a record's own screen - "Add vehicle", "Add booking" at the
    top of their lists. Blue, as every control that takes somebody to another screen is, and the full
    width of the card, so there is no empty stretch beside it the way there was beside a small button
    tucked in the corner.
*/
.entry-add--navigate {
    color: var(--navigate);
    border-color: color-mix(in srgb, var(--navigate) 55%, var(--surface));
    text-decoration: none;
    margin-bottom: var(--space-3);
}

.entry-add--navigate:hover { color: var(--navigate); border-color: var(--navigate); background-color: var(--navigate-surface); }

/* The hint under a section, which on a card no longer needs to sit so close to the button above. */
.form--sectioned .fieldset > .field__hint:last-child { margin: 0; }

/* 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 screen shown while the WebAssembly runtime downloads and starts, before the application has
    drawn anything. Blazor sets --blazor-load-percentage itself as files arrive.

    Centred in the whole screen, on the dark backdrop the landing page uses, so the first paint and
    the loaded page do not look like two different applications.

    THE RING IS WHITE, NOT THE BRAND ACCENT. The accent is chosen to stand out on white, and for a
    monochrome brand like Pickett's it is near black - which drew the progress in almost exactly the
    colour of the backdrop, so the ring could barely be seen.
*/
.loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-5);
    min-height: 100vh;
    min-height: 100dvh;
    padding: var(--space-6) var(--space-4);
    box-sizing: border-box;
    text-align: center;
    color: var(--text-on-dark);
}

.loading__logo { width: 11rem; max-width: 60vw; height: auto; }

.loading__ring { position: relative; width: 6rem; height: 6rem; }

.loading-progress {
    display: block;
    width: 100%;
    height: 100%;
    /* Turning all the time, so it is plain the phone is still working even once every file has
       arrived and the percentage has stopped at 100 while the application starts. */
    animation: loading-spin 1.4s linear infinite;
}

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

/* Never less than a short arc, so there is always something visibly turning - at 0% before
   anything has arrived as well as at 100% while it starts. */
.loading-progress circle:last-child {
    stroke: #ffffff;
    stroke-linecap: round;
    stroke-dasharray: max(calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 12%), 500%;
    transition: stroke-dasharray 0.05s ease-in-out;
}

.loading-progress-text {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 600;
    font-size: 0.9375rem;
    color: var(--text-on-dark);
}

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

/* Once every file has arrived, a full ring turning looks exactly like one standing still. So the
   ring becomes a quarter arc chasing round, and the "100%" goes, while the application starts. */
.loading--starting .loading-progress circle:last-child { stroke-dasharray: 60%, 500%; }

.loading--starting .loading-progress-text { display: none; }

.loading__status { margin: 0; color: var(--text-on-dark-muted); font-size: 0.9375rem; }

.loading__stuck { display: flex; flex-direction: column; align-items: center; gap: var(--space-4); max-width: 20rem; }

.loading__stuck[hidden] { display: none; }

.loading__stuck-text { margin: 0; line-height: 1.5; color: var(--text-on-dark); }

/* Plain CSS rather than the application's button classes, which are not guaranteed to have been
   styled for a page this early - and a thumb-sized white button on the dark backdrop either way. */
.loading__retry {
    min-height: var(--touch-target);
    padding: 0 var(--space-6);
    border: 0;
    border-radius: var(--radius-full);
    background-color: #ffffff;
    color: var(--grey-900);
    font: inherit;
    font-weight: 600;
    font-size: 1rem;
    cursor: pointer;
}

@keyframes loading-spin { to { transform: rotate(360deg); } }

/* Still turning for anybody who has asked for less motion, just slowly: a ring that stops dead is
   exactly what looks like a hang. */
@media (prefers-reduced-motion: reduce) {
    .loading-progress { animation-duration: 4s; }
}

#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(--danger-surface); }

/* 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); }

/* Make main and Remove side by side in a card's header. */
.contact-card__actions { display: inline-flex; align-items: center; gap: var(--space-4); }

/* As quiet as Remove, without Remove's red: moving a number to the top is not a loss of anything. */
.contact-card__action {
    border: none;
    background: none;
    padding: var(--space-2);
    margin: calc(-1 * var(--space-2));
    color: var(--brand-accent);
    cursor: pointer;
    border-radius: var(--radius-sm);
    font-size: 0.875rem;
    font-weight: 500;
}

.contact-card__action:hover { background-color: var(--grey-100); }

/* The label pills sit between the field's name and its box, so they need less room around them. */
.chips--compact { margin-bottom: var(--space-2); }

/* 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); }

/* --- Address search -------------------------------------------------------------------------------
   Royal Mail's address list, searched as somebody types. The results are big tap targets in a plain
   list rather than a dropdown floating over the form: a floating list is fiddly with a thumb and
   hides the fields beneath it, and there is no hover on a phone to preview anything. */
.address-search { display: flex; flex-direction: column; gap: var(--space-2); }

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

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

/* Room for the magnifying glass. */
.address-search__input { padding-left: 2.75rem; }

.address-search__results {
    list-style: none;
    margin: 0;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background-color: var(--surface);
    overflow: hidden;
}

.address-search__results li + li { border-top: 1px solid var(--border); }

.address-search__result {
    display: block;
    width: 100%;
    min-height: var(--touch-target);
    padding: var(--space-3);
    border: none;
    background: none;
    text-align: left;
    font: inherit;
    font-size: 0.9375rem;
    color: var(--text-primary);
    cursor: pointer;
}

.address-search__result:hover,
.address-search__result:focus-visible { background-color: var(--grey-100); outline: none; }

.address-search__result:disabled { color: var(--text-tertiary); cursor: default; }

/* A quiet, link-like button for the way round the usual path: "Can't find it? Type it in yourself".
   Still a full touch target tall, because quiet must not mean hard to hit. */
.text-action {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--touch-target);
    padding: 0;
    border: none;
    background: none;
    font: inherit;
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--text-secondary);
    text-decoration: underline;
    text-underline-offset: 0.2em;
    cursor: pointer;
}

.text-action:hover { 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); }

    /* Its heading follows the wider padding, so it still lines up with the table beneath. */
    .price-table > .card__heading { left: calc(var(--space-5) - var(--space-2)); }
}

/* 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);
    /* The same 24px as between cards, whose headings rise into the space above them. */
    margin: 0 0 var(--space-5);
    padding: var(--space-4);
    background-color: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    /* The same lift as every card, so the strip does not read as flatter than the rest. */
    box-shadow: var(--shadow-1);
}

.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); }

    /* The column heading says it now. Hidden from sight only, the same way as .visually-hidden,
       so a screen reader still hears what the date is. */
    .person-row__seen-label {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
    }
}

/* =================================================================================================
   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));
}

/* The dashboard spaces its sections with its own gap, so a card's usual bottom margin would double it. */
.dashboard > .card { margin-bottom: 0; }

.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);
}

/*
    The secondary actions are compact rows: the icon beside the label, one line tall at a full touch
    target. As tall tiles with the icon above the label, the four of them took most of a phone's
    first screen and pushed the day's work - the thing the screen is for - below the fold.
*/
.quick-action {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    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);
    box-shadow: var(--shadow-1);
    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. */
/*
    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. A
    grid of its own - icon in a column, label with the hint beneath - since it is the one action
    with a line of explanation.
*/
.quick-action--primary {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas: "icon label" "icon hint";
    column-gap: var(--space-3);
    row-gap: 0;
    padding: var(--space-3) var(--space-4);
    background-color: var(--brand-primary);
    border-color: var(--brand-primary);
    color: var(--text-on-dark);
}

.quick-action--primary .quick-action__icon { grid-area: icon; }

.quick-action--primary .quick-action__label { grid-area: label; }

.quick-action--primary .quick-action__hint { grid-area: hint; }

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

.quick-action__icon { width: 1.25rem; height: 1.25rem; flex-shrink: 0; }

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

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

/*
    EVERY ROW FULL, whichever buttons somebody is shown.

    Staff get three, an administrator four (Prices), an owner five (Prices and Team). Fitting as
    many columns as the width allowed left an owner's Team alone on a second row with a gap beside
    it, which reads as something missing. So the columns follow HOW MANY buttons there are, counted
    with :has, and the width only decides between laying them out in one row or two full ones.

    Sized by the dashboard's own width rather than the window's, because the rail takes a different
    share of the screen at each size and it is the room the buttons actually have that matters.
*/
.dashboard { container-type: inline-size; }

/* On a phone, four buttons go two by two: Book spanning the row would leave the last one alone. */
.quick-actions:has(> :nth-child(4):last-child) .quick-action--primary { grid-column: auto; }

@container (min-width: 36rem) {
    /* Three: one row. */
    .quick-actions:has(> :nth-child(3):last-child) { grid-template-columns: repeat(3, minmax(0, 1fr)); }

    .quick-actions:has(> :nth-child(3):last-child) .quick-action--primary { grid-column: auto; }

    /* Five: Book twice as wide beside Add customer, then the other three - three and three. */
    .quick-actions:has(> :nth-child(5):last-child) { grid-template-columns: repeat(3, minmax(0, 1fr)); }

    .quick-actions:has(> :nth-child(5):last-child) .quick-action--primary { grid-column: span 2; }
}

/* Room for everything on one row: as many columns as there are buttons. */
@container (min-width: 60rem) {
    .quick-actions:has(> :nth-child(4):last-child) { grid-template-columns: repeat(4, minmax(0, 1fr)); }

    .quick-actions:has(> :nth-child(5):last-child) { grid-template-columns: repeat(5, minmax(0, 1fr)); }

    .quick-actions:has(> :nth-child(5):last-child) .quick-action--primary { grid-column: auto; }
}

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

/* The card supplies the border, padding and title; this only spaces the header from the rows. */
.day { display: flex; flex-direction: column; gap: var(--space-3); margin-bottom: 0; }

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

/*
    Previous, Today and Next as one joined control on the right, so the buttons never move between
    one day and the next and read as one thing rather than three unrelated boxes.
*/
.day__nav {
    display: inline-flex;
    align-items: stretch;
    flex: 0 0 auto;
    border: 1px solid var(--border);
    border-radius: var(--radius-full);
    background-color: var(--surface);
    box-shadow: var(--shadow-1);
    overflow: hidden;
}

.day__step,
.day__today {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Full touch height. Stepping a day is done one-handed while holding something else. */
    min-height: var(--touch-target);
    border: none;
    background: none;
    color: var(--text-secondary);
    font: inherit;
    cursor: pointer;
}

.day__step { width: var(--touch-target); }

.day__today {
    padding: 0 var(--space-3);
    border-left: 1px solid var(--border);
    border-right: 1px solid var(--border);
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--text-primary);
}

.day__step:hover,
.day__today:hover:not(:disabled) { background-color: var(--grey-50); color: var(--text-primary); }

/* Already on today: still there, so nothing beside it moves, but plainly not something to press. */
.day__today:disabled { color: var(--text-tertiary); cursor: default; }


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

/* Pulled up under the header, so the date and its summary still read as one block. */
.day__meta { margin: calc(-1 * var(--space-2)) 0 0; font-size: 0.875rem; color: var(--text-secondary); }

/* Inside the card now, so no dashed outline of its own - a box drawn in a box reads as clutter. */
.day__empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-5) var(--space-4) var(--space-3);
    border-top: 1px solid var(--grey-100);
    color: var(--text-secondary);
    text-align: center;
}

.day__empty .button { margin-top: var(--space-2); }

.day__empty p { margin: 0; }

/* Mine or Everyone. Capped in width on a wide screen, where two halves of the full card would be
   two very long buttons with a word in the middle of each. */
.day__whose { max-width: 20rem; }

/* --- The month ------------------------------------------------------------------------------- */

/*
    SEVEN COLUMNS THAT FIT A 375PX PHONE, sized from the width rather than fixed.

    Inside a card on a 375px screen there is about 311px across, so each day is about 44px wide:
    the documented minimum for a tap, and as wide as it can be without scrolling sideways. The
    height is the full 48px target, which is where the extra room goes. On a wide screen it is the
    same grid, capped so a day does not become a slab the width of a thumbnail.
*/
.month { display: flex; flex-direction: column; gap: var(--space-2); max-width: 32rem; }

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

.month__title { margin: 0; font-weight: 650; }

.month__step {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    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);
    cursor: pointer;
}

.month__step:hover:not(:disabled) { background-color: var(--grey-50); color: var(--text-primary); }

.month__step:disabled { color: var(--grey-300); cursor: default; }

.month__notice {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    background-color: var(--warning-surface);
    color: var(--warning);
    font-size: 0.875rem;
}

.month__weekdays,
.month__grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 2px; }

.month__weekdays span {
    text-align: center;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--text-tertiary);
}

.month__day {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    min-width: 0;
    min-height: var(--touch-target);
    padding: 0;
    border: none;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text-primary);
    font: inherit;
    cursor: pointer;
}

.month__day:hover:not(:disabled) { background-color: var(--grey-100); }

.month__number { font-size: 0.9375rem; font-weight: 600; font-feature-settings: "tnum"; line-height: 1; }

/* Solid and dark, so the number reads at a glance in daylight: this is the thing being looked for.
   A day with nothing booked has no pill at all, and that absence is what "free" looks like. */
.month__count {
    min-width: 1.25rem;
    padding: 1px var(--space-1);
    border-radius: var(--radius-full);
    background-color: var(--grey-800);
    color: #ffffff;
    font-size: 0.75rem;
    font-weight: 700;
    font-feature-settings: "tnum";
    line-height: 1.2;
    text-align: center;
}

/* Today is ringed rather than filled, so it can be told apart from the day that is chosen. */
.month__day--today { box-shadow: inset 0 0 0 2px var(--navigate); }

.month__day--today .month__number { color: var(--navigate); }

.month__day--selected,
.month__day--selected:hover:not(:disabled) { background-color: var(--brand-accent); color: var(--brand-accent-ink); }

.month__day--selected .month__number { color: inherit; }

.month__day--selected .month__count {
    background-color: var(--brand-accent-ink);
    color: var(--brand-accent);
}

/* Not known: no signal, or still loading. Faded with no number, never drawn like a free day. */
.month__day--unknown .month__number { opacity: 0.35; }

/* Outside the dates that may be booked. */
.month__day:disabled { cursor: default; opacity: 0.3; }

/* The date chosen, beside a label that has no box of its own to show it in. */
.field__chosen { margin-left: var(--space-1); color: var(--text-primary); font-weight: 700; }

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

/* Rows divided by a line, the same as the team list and the price list, rather than a stack of
   separately bordered boxes inside a card. */
.job-list { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--grey-100); }

/*
    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;
    /*
        ON A PHONE: when and plate across the top, who and what beneath with the price opposite,
        then the access note and the call button each across the full width.

        Three columns side by side left the middle one - the customer, the service, the address -
        about a third of a phone's width, so every line wrapped and the note became a tall narrow
        strip. Stacking gives the words the room, and keeps the time and plate in the same place on
        every card, which is what the eye checks first.
    */
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
        "when   side"
        "body   side"
        "where  where"
        "access access"
        "people call";
    column-gap: var(--space-3);
    row-gap: var(--space-2);
    align-items: start;
    /* Pulled out to the card's edges so the hover wash and the dividers run its full width, with
       the content kept in line with the card's other contents. */
    margin: 0 calc(-1 * var(--space-4));
    padding: var(--space-4) var(--space-4) var(--space-4) calc(var(--space-4) + var(--space-1));
    border-bottom: 1px solid var(--grey-100);
    position: relative;
}

/* The status stripe down the left of each row: accent for booked, blue while being worked, green
   for done, grey for cancelled. A bar in the row rather than the row's own border, now that rows have no border. */
.job-card::before {
    content: "";
    position: absolute;
    left: var(--space-2);
    top: var(--space-4);
    bottom: var(--space-4);
    width: 3px;
    border-radius: var(--radius-full);
    background-color: var(--brand-accent);
}

/* No access note, no row for one. An empty row still takes a gap above and below it, which is
   dead space in the middle of most cards - most jobs have no note. */
.job-card:not(:has(.job-card__access)) {
    grid-template-areas:
        "when   side"
        "body   side"
        "where  where"
        "people call";
}

.job-card:last-child { border-bottom: none; padding-bottom: var(--space-2); }

/* 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::before { background-color: #15803d; }

/* Blue while somebody is on it, matching its badge, so a day half worked through reads as green,
   then blue, then the rest still to do. */
.job-card--doing::before { background-color: #1d4ed8; }

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

.job-card--cancelled { opacity: 0.6; }

.job-card--cancelled::before { background-color: var(--grey-300); }

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

/* Time and duration on one line on a phone, so they take the top row rather than a column. */
.job-card__when { grid-area: when; display: flex; align-items: baseline; gap: var(--space-2); }

.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; }

/* As wide as the name and no wider. As a flex item it stretched across the card, so tapping the
   empty space beside a short name went to the customer instead of the job. */
.job-card__who { align-self: flex-start; font-size: 1.0625rem; 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); }

/* Its own row, pulled up to sit as close under the service as it did when it was inside the body
   block, rather than a whole row gap away. */
.job-card__where {
    grid-area: where;
    margin-top: calc(2px - var(--space-2));
    font-size: 0.8125rem;
    color: var(--text-tertiary);
}

/* Who is on the job. Beside the call button on a phone, under the address on a wide screen. */
.job-card__people { grid-area: people; align-self: center; display: flex; flex-wrap: wrap; gap: var(--space-1); }

/*
    A person's initials in a circle. Sized to be read rather than tapped - it is a label, and the
    card around it is the thing to tap.

    Filled with the company's accent, the same as a chosen chip in the picker, so the people on a
    job read as the ones that were chosen. It was pale grey, which disappeared against a white card.
*/
.initials {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.75rem;
    height: 1.75rem;
    padding: 0 var(--space-1);
    border-radius: var(--radius-full);
    background-color: var(--brand-accent);
    color: var(--brand-accent-ink);
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.02em;
}

/* 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 {
    grid-area: access;
    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;
}

/* Right-aligned with everything under it. The general rule that stops a plate stretching pins it
   to the START of a column, which here is the left, so a plate narrower than the make and model
   beneath it - "No plate" above "Land Rover Defender" - sat visibly short of the right edge. */
.job-card__side .plate { align-self: flex-end; }

.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, along the bottom of the card on a phone.

    In the flow with a named grid area, never absolutely placed: an absolute button either covers
    text or needs the card padded around a guess at its size, and an unplaced one falls into
    whatever cell is left over - which is how it ended up stranded under the time. Labelled as well
    as drawn, because a bare telephone icon in a circle reads as decoration until somebody learns
    it. A tel: link works with no signal: the dialler is on the phone, not on the server.
*/
/* Maps and Call side by side, in the call button's place on the card. */
.job-card__actions {
    grid-area: call;
    justify-self: end;
    display: flex;
    gap: var(--space-2);
}

.job-card__call {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    /* The full touch target. 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. */
    min-height: var(--touch-target);
    padding: 0 var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-full);
    background-color: var(--surface);
    color: var(--text-primary);
    font-size: 0.875rem;
    font-weight: 600;
    text-decoration: none;
}

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

/*
    Maps as an icon-only circle, always. On a phone it shares its row with the people on the job,
    and with a label as well as Call's it squeezed "Nobody on it yet" down to "Nobody on it ye".
    The arrow is the familiar navigation symbol, and the button still names itself to a screen
    reader. Call keeps its word, because dialling the wrong person is the costlier mis-tap.
*/
.job-card__call--icon { width: var(--touch-target); padding: 0; }

.job-card__call--icon .job-card__call-label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
}

/* The Maps button is a <button> until this phone has chosen an app. */
button.job-card__call { font: inherit; font-size: 0.875rem; font-weight: 600; cursor: pointer; }

/* --- Directions ----------------------------------------------------------------------------------
   An address that opens driving directions. It still reads as an address, with an underline to say
   it can be tapped - as a link or, before an app is chosen, as a button made to look like one. */
.directions-link {
    display: inline;
    padding: 0;
    border: none;
    background: none;
    font: inherit;
    color: inherit;
    text-align: left;
    text-decoration: underline;
    text-decoration-color: var(--grey-400);
    text-underline-offset: 0.2em;
    cursor: pointer;
}

.directions-link:hover { text-decoration-color: currentColor; }

.maps-note { display: flex; align-items: center; gap: var(--space-2); margin: 0; font-size: 0.8125rem; color: var(--text-secondary); }

.maps-note__change {
    min-height: var(--touch-target);
    padding: 0 var(--space-2);
    border: none;
    background: none;
    font: inherit;
    font-weight: 500;
    color: var(--brand-accent);
    text-decoration: underline;
    cursor: pointer;
}

/* Asked once per phone: which app. Up from the bottom, where a thumb already is. */
.maps-sheet { position: fixed; inset: 0; z-index: 100; display: flex; align-items: flex-end; justify-content: center; }

.maps-sheet__backdrop { position: absolute; inset: 0; background-color: rgb(0 0 0 / 0.4); }

.maps-sheet__panel {
    position: relative;
    width: 100%;
    max-width: 28rem;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-5) var(--space-4) calc(var(--space-4) + env(safe-area-inset-bottom));
    border-radius: var(--radius) var(--radius) 0 0;
    background-color: var(--surface);
    color: var(--text-primary);
    text-align: left;
    white-space: normal;
}

.maps-sheet__title { margin: 0; font-size: 1.0625rem; font-weight: 600; }

.maps-sheet__cancel { align-self: center; }

/* --- 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; }

/* The day a job is on, above its time, for a list that spans more than one day. */
.job-card__date { font-size: 0.8125rem; font-weight: 600; color: var(--text-secondary); white-space: nowrap; }

/* --- The bookings list ------------------------------------------------------------------------- */

/* The Filters button and the filters that are on, in one row that wraps. */
.booking-filters { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-4); }

/* A filter that is on, with the cross that clears it inside the chip rather than beside it, so the
   whole chip is the target: one tap, however cold the hands. */
.chip--clearable { display: inline-flex; align-items: center; gap: var(--space-2); }

/* Filters and Sort: an icon before the words, so the two are told apart without reading. */
.chip--icon { display: inline-flex; align-items: center; gap: var(--space-2); }

.booking-filter-panel { display: flex; flex-direction: column; gap: var(--space-4); }

.booking-filter { margin: 0; padding: 0; border: none; min-width: 0; }

.booking-filter__label { margin-bottom: var(--space-2); padding: 0; font-size: 0.875rem; font-weight: 600; color: var(--text-secondary); }

.booking-filter__dates { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-3); }

/* The job list's top rule separates it from the day view's heading. First in its card, as on the
   bookings list, there is nothing above it to separate, and the rule read as an empty row. */
.card > .job-list:first-child { border-top: none; }

.card > .job-list:first-child > .job-card:first-child { padding-top: var(--space-2); }

/* The count and the total, where the eye lands before the list. */
.booking-summary { margin: 0 0 var(--space-3); font-weight: 600; font-feature-settings: "tnum"; }

/* No signal: the message, then the way to try again, set apart so the button does not read as
   part of the warning. */
.booking-offline { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-4); }

.booking-offline .notice { align-self: stretch; margin: 0; }

.booking-more { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); margin-bottom: var(--space-5); }

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

@media (min-width: 48rem) {
    /*
        Room for everything on one line: when, who, what it is and costs, and the call button, with
        any access note running underneath the middle two. Every area is named, so nothing is left
        to fall wherever the grid puts it.
    */
    .job-card {
        grid-template-columns: 5.5rem minmax(0, 1fr) auto auto;
        grid-template-areas:
            "when body   side   call"
            ".    where  where  ."
            ".    people people ."
            ".    access access .";
        column-gap: var(--space-4);
        align-items: center;
    }

    .job-card:not(:has(.job-card__access)) {
        grid-template-areas:
            "when body   side   call"
            ".    where  where  ."
            ".    people people .";
    }

    .job-card__when { flex-direction: column; align-items: flex-start; gap: 2px; align-self: start; }

    /* An icon-only circle once it sits in its own column, where its place explains it. */
    .job-card__call { width: var(--touch-target); padding: 0; }

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

/* =================================================================================================
   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;
}

/* Above the stretched link, so each keeps its own tap. Relative positioning only - none of these is
   placed by it, which is what let the call button escape before. */
.job-card__body,
.job-card__side,
.job-card__when,
.job-card__access,
.job-card__people,
.job-card__where,
.job-card__actions { position: relative; z-index: 1; }

/*
    TAPS FALL THROUGH THE TEXT to the job underneath, and only the real controls catch them.

    The blocks sit above the stretched link so the name and the call button can win, but that meant
    the blocks also swallowed every other tap: pressing the service or the address did nothing at
    all. Letting taps through the blocks, and back on for links and buttons inside them, makes
    everything on the card open the job except the two things that go somewhere else.
*/
.job-card__body,
.job-card__side,
.job-card__when,
.job-card__access,
.job-card__where,
.job-card__people { pointer-events: none; }

.job-card__body a,
.job-card__body button { pointer-events: auto; }

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

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

/* Who is on a job, by full name, for anybody who cannot change it. */
.job-people { display: flex; flex-direction: column; gap: var(--space-2); margin: 0; padding: 0; list-style: none; }

.job-people__person { display: flex; align-items: center; gap: var(--space-2); font-weight: 500; }

/*
    ONE SYSTEM FOR THE WHOLE SCREEN: a header, then identical cards one gap apart.

    The full width of the page, the same as the customer and vehicle screens either side of it, so
    opening a job from them does not jump to a narrower column.
*/
.job-screen__message { margin: 0 0 var(--space-4); }

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

/* The card every section is. Its heading sits on the top edge like every other card's - see
   .card__heading. */
.panel {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding: var(--space-4);
    border: 1px solid var(--border);
    /* The customer screen's card corners and lift, so the job screen is drawn the same way. */
    border-radius: var(--radius);
    box-shadow: var(--shadow-1);
    background-color: var(--surface);
    min-width: 0;
}

/* Styled and placed with .card__heading. */

/* Inside a card a field is as wide as the card: the 34rem cap exists for page-wide forms, and here
   it only made one box stop short of the button beneath it. */
.job-screen .panel .field > .input { max-width: none; }

/*
    Every control on this screen is one height. Things side by side - the amount and its button, the
    date and "Move it" - are the same height to the pixel, and a lone button is the same height as
    the box above it. border-box so the border is inside the 48px rather than added to it.
*/
.job-screen .panel .input:not(textarea),
.job-screen .panel .button {
    box-sizing: border-box;
    height: var(--touch-target);
    min-height: var(--touch-target);
}

.job-screen__row { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }

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

/* Wide enough for "14:30" and the picker's clock icon, and no wider, so the date keeps the room. */
.job-screen__time { flex: 0 0 7rem; width: 7rem; }

/* The day to move to, as a box that opens the month rather than the phone's own date wheel. */
.job-screen__day { display: inline-flex; align-items: center; gap: var(--space-2); text-align: left; cursor: pointer; }

/* Date, time and button do not fit one row on a phone. Wrapping drops the button underneath rather
   than squeezing the date until it cannot show a whole day. */
.job-screen__row--wrap { flex-wrap: wrap; }
.job-screen__row--wrap .job-screen__grow { min-width: 9rem; }

/* Capped now the card runs the full page width: stretched across it, the date pushed the time and
   "Move it" onto a line of their own, a whole card's width away from the day they belong to. */
.job-screen__row--wrap .job-screen__grow { max-width: 20rem; }

.job-screen__pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }

/* A full-width row like every other action, rather than an indented ghost, and coloured so it does
   not read as routine. */

/* Deliberately quiet: a text link under Cancel Job, still a full thumb's height to tap. */
.job-screen__quiet-link {
    display: block;
    width: 100%;
    min-height: var(--touch-target);
    margin-top: var(--space-1);
    border: none;
    background: none;
    color: var(--text-secondary);
    font: inherit;
    font-size: 0.875rem;
    text-decoration: underline;
    cursor: pointer;
}

/*
    Everything that deletes or cancels, as a red outline with red words rather than a solid red
    block.

    Red says "this destroys something" without shouting it. A solid red button was the loudest thing
    on any screen it appeared on - louder than Complete Job or Save - which pulled the eye to the one
    control nobody should press by accident. The confirmation step in front of it is what stops a
    mistake; the colour only has to say what kind of button it is.

    .button--destructive and .job-screen__destructive are the first press (Delete customer, Cancel
    Job), .button--danger the confirmation. They look the same on purpose.
*/
.button--danger,
.button--destructive,
.job-screen__destructive {
    background-color: var(--surface);
    border-color: var(--danger);
    color: var(--danger);
    box-shadow: none;
}

.button--danger:hover:not(:disabled),
.button--destructive:hover:not(:disabled),
.job-screen__destructive:hover:not(:disabled) {
    background-color: var(--danger-surface);
    border-color: var(--danger);
}

/* The Delete card's "Customer / Photos only" choice, in the same red, so what it leads to is clear
   before it is tapped. Chosen, it fills with the pale red rather than the brand colour every other
   selected pill uses. */
.chip--danger { border-color: var(--danger); color: var(--danger); }

.chip--danger.chip--selected {
    background-color: var(--danger-surface);
    border-color: var(--danger);
    color: var(--danger);
}

/* On the photo viewer's black background the page's red is too dark to read, so a lighter red. */
.photo-viewer .button--danger {
    background-color: transparent;
    border-color: #ff8a80;
    color: #ff8a80;
}

.photo-viewer .button--danger:hover:not(:disabled) { background-color: rgba(255, 138, 128, 0.12); }

/* The vehicle: plate and model on one line, then where it is under a rule. */
.job-detail { gap: var(--space-4); }

.job-detail__vehicle { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }

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

.job-detail__model { font-weight: 600; line-height: 1.3; }

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

.job-detail__where {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    min-width: 0;
    padding-top: var(--space-4);
    border-top: 1px solid var(--border);
}

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

/* The customer's own notes on the job, set apart from the access notes above them: those are about
   getting in, these are about the customer - "pays via invoice". Neutral rather than amber, so the
   gate code stays the thing that stands out. */
.job-detail__customer-notes {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-2);
    border-radius: var(--radius-sm);
    background-color: var(--grey-100);
    color: var(--text-secondary);
    font-size: 0.8125rem;
}

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

/* The address's label sits in line with it, a little apart. */
.job-detail__address .badge,
.job-card__where .badge { margin-right: var(--space-1); vertical-align: 0.1em; }

/* The job's figures reuse the vehicle-size tiles: the same "pick one of two, with a line saying what
   it means" shape. A card payment has one figure, which takes the whole row rather than half. */
.size-grid--single { grid-template-columns: 1fr; }

.job-screen .charge__amount { gap: var(--space-3); }

.job-screen__other-amount { margin-top: var(--space-2); }

.job-screen .charge__input { flex: 0 0 6.5rem; width: 6.5rem; }

/* --- 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:first-child { padding-top: 0; }

.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: #1d4ed8; border-color: #1d4ed8; }

.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" "people 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);
}

/* Who is on the job, under what it was. The same initials as the day view. */
.timeline__people { grid-area: people; display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: var(--space-1); }

.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. */
/* A page heading with an Edit button beside it. The heading keeps its own spacing, and the button
   sits level with its first line rather than centred on a heading that wraps onto two. */
.page__title-row { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); }
.page__title-row .page__title { min-width: 0; }
.page__title-row .button { flex: 0 0 auto; }

.page__actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-4); }

/* =================================================================================================
   Change your password
   ================================================================================================= */

.page__lede { margin: calc(-1 * var(--space-3)) 0 var(--space-5); color: var(--text-secondary); max-width: 34rem; }

/* A full touch target, because it is pressed with a thumb between two rounds of typing. */
.password-toggle {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--touch-target);
    padding: 0 var(--space-3);
    margin-left: calc(-1 * var(--space-3));
    background: none;
    border: none;
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    font: inherit;
    font-size: 0.9375rem;
    cursor: pointer;
}

.password-toggle:hover { color: var(--text-primary); background-color: var(--grey-100); }

.checklist {
    list-style: none;
    margin: 0;
    padding: var(--space-3) var(--space-4);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    background-color: var(--surface-sunken);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    max-width: 34rem;
}

.checklist__item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    color: var(--text-secondary);
    font-size: 0.9375rem;
    transition: color 150ms ease;
}

/* A filled disc rather than just a coloured tick, so "met" is obvious in sunlight and to anybody who
   cannot tell green from grey - the shape changes, not only the colour. */
.checklist__mark {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: var(--radius-full);
    color: var(--grey-400);
    transition: background-color 150ms ease, color 150ms ease;
}

.checklist__item--met { color: var(--success); font-weight: 500; }

.checklist__item--met .checklist__mark { background-color: var(--success); color: #ffffff; }

.checklist__progress { color: var(--text-tertiary); font-weight: 400; margin-left: var(--space-1); }

.success-panel {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-3);
    max-width: 34rem;
    padding: var(--space-5);
    background-color: var(--success-surface);
    border: 1px solid color-mix(in srgb, var(--success) 25%, transparent);
    border-radius: var(--radius-lg);
}

.success-panel__mark {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: var(--radius-full);
    background-color: var(--success);
    color: #ffffff;
}

.success-panel__title { margin: 0; font-size: 1.25rem; font-weight: 650; color: var(--success); }

.success-panel__text { margin: 0; color: var(--text-primary); }

@media (prefers-reduced-motion: reduce) {
    .checklist__item, .checklist__mark { transition: none; }
}

/* Whoever is signed in is a link to their own password, in both navigations. Inherits the bar's
   colours rather than turning into a blue underlined link on a dark background. */
a.rail__me { color: inherit; text-decoration: none; }

.rail__me {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
    padding: var(--space-1);
    margin: calc(-1 * var(--space-1));
    border-radius: var(--radius-sm);
}

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

.rail__me-text { display: flex; flex-direction: column; min-width: 0; }

.rail__me-hint { font-size: 0.75rem; opacity: 0.7; }

/* =================================================================================================
   Job photos
   ================================================================================================= */

/* Two columns, before and after, each a camera button with its stack underneath. Side by side
   even on a phone: both are needed on every job, and the thumb reaches either. */
.photo-columns { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }

.photo-columns__column { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }

.photos__take { cursor: pointer; gap: var(--space-2); padding-left: var(--space-3); padding-right: var(--space-3); font-size: 0.9375rem; }

.photos__take--disabled { opacity: 0.6; pointer-events: none; }

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

/*
    The stack. The newest photo on top; with more than one, two cards peek out behind it, fanned a
    few degrees, which says "there are more" before anybody reads the count.
*/
.photo-stack {
    position: relative;
    display: block;
    width: 100%;
    aspect-ratio: 4 / 3;
    max-width: 100%;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background-color: var(--grey-100);
    cursor: pointer;
    font: inherit;
}

.photo-stack--many { isolation: isolate; }

.photo-stack--many::before,
.photo-stack--many::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: var(--radius);
    background-color: var(--grey-200);
    border: 1px solid var(--border-strong);
    box-shadow: var(--shadow-1);
    z-index: -1;
}

.photo-stack--many::before { transform: rotate(-3deg); }

.photo-stack--many::after { transform: rotate(2deg); background-color: var(--grey-300); }

.photo-stack__image {
    position: relative;
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: calc(var(--radius) - 1px);
}

.photo-stack__count {
    position: absolute;
    right: var(--space-2);
    bottom: var(--space-2);
    min-width: 1.75rem;
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-full);
    background-color: rgba(23, 24, 26, 0.8);
    color: #ffffff;
    font-size: 0.8125rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    text-align: center;
}

.photo-stack--empty {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-3);
    border-style: dashed;
    background-color: var(--surface-sunken);
    color: var(--text-tertiary);
    font-size: 0.8125rem;
    text-align: center;
    cursor: default;
}

.photo-stack:focus-visible { outline: 3px solid var(--brand-accent); outline-offset: 3px; }

/* Nothing behind a full-screen photo moves while it is being swiped. */
html.scroll-locked, html.scroll-locked body { overflow: hidden; overscroll-behavior: none; }

/* ---------------------------------------------------------------- the grid of every photo */

.photo-gallery,
.photo-viewer {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    flex-direction: column;
    background-color: var(--surface-dark);
    color: var(--text-on-dark);
    padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
    outline: none;
}

.photo-gallery__bar,
.photo-viewer__bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
    flex: 0 0 auto;
}

.photo-gallery__title { margin: 0; font-size: 1.0625rem; font-weight: 650; display: flex; align-items: center; gap: var(--space-2); }

.photo-gallery__total {
    padding: 0 var(--space-2);
    border-radius: var(--radius-full);
    background-color: rgba(255, 255, 255, 0.14);
    font-size: 0.8125rem;
    font-variant-numeric: tabular-nums;
}

.photo-gallery__grid {
    flex: 1 1 auto;
    overflow-y: auto;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
    align-content: start;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4) var(--space-5);
}

.photo-gallery__item {
    display: block;
    aspect-ratio: 1;
    max-width: 100%;
    padding: 0;
    border: none;
    border-radius: var(--radius-sm);
    overflow: hidden;
    background-color: var(--surface-dark-raised);
    cursor: pointer;
}

.photo-gallery__item img { width: 100%; height: 100%; object-fit: cover; display: block; }

.photo-gallery__item:focus-visible { outline: 3px solid #ffffff; outline-offset: 2px; }

/* ---------------------------------------------------------------- one photo, full screen */

.photo-viewer { background-color: #000000; touch-action: pan-y pinch-zoom; }

.photo-viewer__count { font-size: 0.9375rem; font-weight: 600; font-variant-numeric: tabular-nums; }

.photo-viewer__close,
.photo-viewer__nav {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--touch-target);
    height: var(--touch-target);
    border: none;
    border-radius: var(--radius-full);
    background-color: rgba(255, 255, 255, 0.14);
    color: #ffffff;
    cursor: pointer;
}

.photo-viewer__close:hover,
.photo-viewer__nav:hover:not(:disabled) { background-color: rgba(255, 255, 255, 0.24); }

.photo-viewer__stage {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}

.photo-viewer__image {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    animation: photo-in 160ms ease-out;
    user-select: none;
    -webkit-user-drag: none;
}

@keyframes photo-in {
    from { opacity: 0.35; transform: scale(0.985); }
    to { opacity: 1; transform: none; }
}

.photo-viewer__nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
}

.photo-viewer__nav--previous { left: max(var(--space-3), env(safe-area-inset-left, 0px)); }

.photo-viewer__nav--next { right: max(var(--space-3), env(safe-area-inset-right, 0px)); }

.photo-viewer__nav:disabled { opacity: 0.25; cursor: default; }

.photo-viewer__caption {
    margin: 0;
    padding: var(--space-3) var(--space-4) var(--space-4);
    text-align: center;
    color: var(--text-on-dark-muted);
    font-size: 0.875rem;
}

@media (prefers-reduced-motion: reduce) {
    .photo-viewer__image { animation: none; }
}

/*
    Phones narrower than 375px - the original iPhone SE and small Androids.

    Two side-by-side buttons there have 120 to 140px each, which fits "Before" with room to breathe but
    not the word after it. The trailing word goes rather than the label touching the edge of its
    button: the camera icon and the section heading already say "photo".
*/
@media (max-width: 374px) {
    .narrow-hidden { display: none; }

    .job-screen .charge__input { flex-basis: 5.5rem; width: 5.5rem; }
}

/* =================================================================================================
   A new version is ready
   ================================================================================================= */

/* Built by bootstrap.js outside the Blazor application, so it is styled here from nothing. Sits at
   the bottom where a thumb already is, clear of the home indicator, and above everything else on the
   page including an open photo. */
.app-update {
    position: fixed;
    left: max(var(--space-4), env(safe-area-inset-left, 0px));
    right: max(var(--space-4), env(safe-area-inset-right, 0px));
    bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
    z-index: 200;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    max-width: 34rem;
    margin: 0 auto;
    padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4);
    border-radius: var(--radius-lg);
    background-color: var(--surface-dark);
    color: var(--text-on-dark);
    box-shadow: var(--shadow-3);
    font-size: 0.9375rem;
}

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

.app-update__button {
    flex: 0 0 auto;
    height: var(--touch-target);
    padding: 0 var(--space-5);
    border: none;
    border-radius: var(--radius);
    background-color: #ffffff;
    color: var(--grey-900);
    font: inherit;
    font-weight: 700;
    cursor: pointer;
}

.app-update__button:disabled { opacity: 0.7; cursor: progress; }

/* =================================================================================================
   Deleting a customer's photos
   ================================================================================================= */

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



.delete-photos__rule,
.delete-photos__result,
.delete-photos__warning { margin: 0; font-size: 0.9375rem; }

.delete-photos__rule { color: var(--text-secondary); }

.delete-photos__result { font-weight: 600; }

.delete-photos__confirm {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-3);
    border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent);
    border-radius: var(--radius);
    background-color: var(--danger-surface);
}

.delete-photos__warning { color: var(--danger); font-weight: 600; }

.delete-photos__pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }

/* A full-width outline in the danger colour: clearly a button, clearly not a routine one. */

/* =================================================================================================
   Your account menu, behind the initials in the top bar
   ================================================================================================= */

.account-menu { position: relative; }

/* Invisible, covering the screen, so a tap anywhere outside the menu closes it. */
.account-menu__backdrop {
    position: fixed;
    inset: 0;
    z-index: 40;
    padding: 0;
    border: none;
    background: transparent;
    cursor: default;
}

.account-menu__panel {
    position: absolute;
    top: calc(100% + var(--space-2));
    right: calc(-1 * var(--space-2));
    z-index: 41;
    display: flex;
    flex-direction: column;
    width: min(17rem, calc(100vw - 2 * var(--space-4)));
    padding: var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background-color: var(--surface);
    color: var(--text-primary);
    box-shadow: var(--shadow-3);
    animation: account-menu-in 120ms ease-out;
}

@keyframes account-menu-in {
    from { opacity: 0; transform: translateY(-4px); }
    to { opacity: 1; transform: none; }
}

.account-menu__who {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    padding: var(--space-2) var(--space-3) var(--space-3);
    margin-bottom: var(--space-1);
    border-bottom: 1px solid var(--border);
}

.account-menu__name { font-weight: 650; font-size: 0.9375rem; }

.account-menu__email {
    font-size: 0.8125rem;
    color: var(--text-secondary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Each item a full touch target, left-aligned, icon and label on one line. */
.account-menu__item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    min-height: var(--touch-target);
    padding: 0 var(--space-3);
    border: none;
    border-radius: var(--radius);
    background: none;
    color: var(--text-primary);
    font: inherit;
    font-size: 0.9375rem;
    text-align: left;
    text-decoration: none;
    cursor: pointer;
}

.account-menu__item:hover,
.account-menu__item:focus-visible { background-color: var(--grey-100); outline: none; }

.account-menu__item .icon { color: var(--text-tertiary); }

.account-menu__divider { height: 1px; margin: var(--space-1) var(--space-2); background-color: var(--border); }

.account-menu__item--signout { color: var(--danger); }

.account-menu__item--signout .icon { color: var(--danger); }

@media (prefers-reduced-motion: reduce) {
    .account-menu__panel { animation: none; }
    .topbar__initials { transition: none; }
}

/* =================================================================================================
   A vehicle's photo history
   ================================================================================================= */

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

.vehicle-photos__rule { margin: 0; font-size: 0.875rem; }



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

.vehicle-photos__visit {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3) 0;
    border-top: 1px solid var(--border);
}

.vehicle-photos__visit:first-child { border-top: none; padding-top: 0; }

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

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

.vehicle-photos__date { font-weight: 650; color: var(--text-primary); text-decoration: none; }

.vehicle-photos__date:hover { text-decoration: underline; }

.vehicle-photos__summary { font-size: 0.8125rem; color: var(--text-secondary); white-space: nowrap; }

/*
    One row per visit, scrolling sideways when a visit has more than fits. A row rather than a wrapping
    grid so every visit takes the same height, and the dates run down the page evenly.
*/
.vehicle-photos__strip {
    display: flex;
    gap: var(--space-2);
    overflow-x: auto;
    scroll-snap-type: x proximity;
    padding-bottom: var(--space-1);
    -webkit-overflow-scrolling: touch;
}

.vehicle-photos__thumb {
    position: relative;
    flex: 0 0 5.5rem;
    width: 5.5rem;
    aspect-ratio: 1;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    overflow: hidden;
    background-color: var(--grey-100);
    cursor: pointer;
    scroll-snap-align: start;
}

.vehicle-photos__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

.vehicle-photos__thumb:focus-visible { outline: 3px solid var(--brand-accent); outline-offset: 2px; }

/* Which kind each one is, on the photograph itself, so a strip of mixed before and after reads at a
   glance without a separate legend. */
.vehicle-photos__kind {
    position: absolute;
    left: var(--space-1);
    bottom: var(--space-1);
    padding: 1px var(--space-1);
    border-radius: var(--radius-sm);
    background-color: rgba(23, 24, 26, 0.75);
    color: #ffffff;
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.02em;
}

/* =================================================================================================
   Photos waiting on the phone
   ================================================================================================= */

/* Under the top bar on every screen, while anything is waiting. Amber rather than red: nothing is
   wrong, something is simply not done yet. */
.outbox-status {
    position: sticky;
    top: var(--header-height);
    z-index: 15;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-2) max(var(--space-4), env(safe-area-inset-right, 0px)) var(--space-2) max(var(--space-4), env(safe-area-inset-left, 0px));
    background-color: var(--warning-surface);
    border-bottom: 1px solid color-mix(in srgb, var(--warning) 30%, transparent);
    color: var(--warning);
    font-size: 0.9375rem;
    font-weight: 600;
}

.outbox-status__text { display: inline-flex; align-items: center; gap: var(--space-2); min-width: 0; }

.outbox-status__text--failed { color: var(--danger); }

.outbox-status__action {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    min-height: 2.5rem;
    padding: 0 var(--space-3);
    border: 1px solid currentColor;
    border-radius: var(--radius);
    background: var(--surface);
    color: inherit;
    font: inherit;
    font-size: 0.875rem;
    text-decoration: none;
    cursor: pointer;
}

.outbox-status__action:disabled { opacity: 0.7; cursor: progress; }

/* Where the rail replaces the top bar, there is no bar above it to sit under. */
@media (min-width: 1024px) {
    .outbox-status { top: 0; }
}

/* On a stack holding photographs not yet sent. */
.photo-stack__waiting {
    position: absolute;
    left: var(--space-2);
    top: var(--space-2);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-full);
    background-color: var(--warning);
    color: #ffffff;
    font-size: 0.75rem;
    font-weight: 700;
}

.photo-failed {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3);
    border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent);
    border-radius: var(--radius);
    background-color: var(--danger-surface);
    color: var(--danger);
    font-size: 0.875rem;
}

.photo-failed__text { min-width: 0; }

.photo-viewer__waiting {
    display: inline-block;
    margin-left: var(--space-2);
    padding: 0 var(--space-2);
    border-radius: var(--radius-full);
    background-color: var(--warning);
    color: #ffffff;
    font-size: 0.75rem;
    font-weight: 700;
}

.account-menu__note {
    margin: var(--space-1) var(--space-3) var(--space-2);
    font-size: 0.8125rem;
    color: var(--warning);
}


/* =================================================================================================
   Deleting one photograph, from the full-screen viewer
   ================================================================================================= */

.photo-viewer__tools { display: inline-flex; align-items: center; gap: var(--space-1); }

/* Where the caption was, so the question sits under the thumb that asked it. */
.photo-viewer__confirm {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4) var(--space-4);
    flex: 0 0 auto;
}

.photo-viewer__confirm-text { margin: 0; text-align: center; font-weight: 600; }

.photo-viewer__confirm-error { margin: 0; text-align: center; color: #ffb4ab; font-size: 0.875rem; }
