/*
 * Layout and components. Not one literal colour, size, spacing or radius in this file - every
 * value comes from tokens.css, and DesignTokenDisciplineTest fails the build when one does
 * not.
 *
 * Structural CSS only: what things are, not what they look like. The visual decisions live in
 * the tokens, so adopting SNOGARD's eventual design means editing that file and leaving this
 * one alone (Q-35).
 */

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

html {
    /* No fixed font size. The browser's own setting is somebody's accessibility preference,
       and overriding it here is the single most effective way to make a page unreadable for
       the people who changed it. */
    -webkit-text-size-adjust: 100%;
}

body {
    margin: 0;
    background: var(--colour-page);
    color: var(--colour-text);
    font-family: var(--font-family-base);
    font-size: var(--font-size-base);
    line-height: var(--line-height-base);
}

/* --------------------------------------------------------------------- skeleton */

.page {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

.page__main {
    flex: 1;
    width: 100%;
    max-width: var(--layout-max-width);
    margin: 0 auto;
    padding: var(--space-lg) var(--layout-gutter) var(--space-2xl);
}

/*
    The header carries the company; the footer carries the small print. They used to share a
    rule and a surface, and now only share a width.
*/
.page__header {
    background: var(--colour-header);
    color: var(--colour-header-text);
}

.page__footer {
    background: var(--colour-surface-sunken);
    border-top: var(--border-width) solid var(--colour-border);
    color: var(--colour-text-muted);
    font-size: var(--font-size-sm);
}

.page__header-inner,
.page__footer-inner {
    max-width: var(--layout-max-width);
    margin: 0 auto;
    padding: var(--space-md) var(--layout-gutter);
}

/*
    The logo and the word beside it, as one link.

    One link rather than two, because they name one thing: a customer who clicks either half
    means "take me to the start". The image carries the company name, so the word beside it is
    only "Serviceportal" - repeating SNOGARD in text would have a screen reader read the name
    twice in a row.

    The height is what fixes the layout: the file is 400x82, and a width would let a
    re-exported logo of different proportions change the height of the header.
*/
.page__brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    font-weight: var(--font-weight-bold);
    font-size: var(--font-size-lg);
    color: var(--colour-header-text);
    text-decoration: none;
}

.page__brand img {
    width: auto;
    height: var(--layout-logo);
}

/*
 * The skip link. Off screen until focused, which is the whole point: a keyboard user should
 * not have to tab through the header on every step of a seven-step form.
 */
.skip-link {
    position: absolute;
    left: var(--space-xs);
    top: var(--space-xs);
    z-index: 10;
    padding: var(--space-xs) var(--space-sm);
    background: var(--colour-accent);
    color: var(--colour-accent-contrast);
    border-radius: var(--radius-sm);
    transform: translateY(-200%);
    transition: transform var(--transition-fast);
}

.skip-link:focus {
    transform: translateY(0);
}

/* ----------------------------------------------------------------- typography */

h1,
h2,
h3 {
    line-height: var(--line-height-tight);
    margin: 0 0 var(--space-sm);
}

/*
    The vertical rhythm, as one rule.

    A heading has always carried space *below* it and none above, which is right for the first
    heading on a page and wrong for every one after it: whatever precedes it - a paragraph, a
    table, a row of buttons - decides the gap, so the gap depends on what happens to come before.
    That is how "Lage" ended up against the navigation, how "Heute — Donnerstag" ended up against
    the button row, and how the heading on the booking confirmation ended up against the block
    above it. Three pages, one cause.

    Fixed once, here, rather than by adding a margin to each page as it is noticed. `* + h1`
    matches a heading that follows a sibling and leaves a first heading alone, so no page needs
    to know about this and no page can forget it.

    The customer-facing pages get it too. The fault was never specific to the backend, and two
    rhythms for one company would be the thing tokens.css exists to prevent.
*/
* + h1,
* + h2,
* + h3 {
    margin-top: var(--space-section);
}

h1 {
    font-size: var(--font-size-page-title);
    line-height: var(--line-height-page-title);
    font-weight: var(--font-weight-bold);
}

h2 {
    font-size: var(--font-size-section);
    line-height: var(--line-height-section);
    font-weight: var(--font-weight-bold);
}

h3 {
    font-size: var(--font-size-card-title);
    line-height: var(--line-height-card-title);
    font-weight: var(--font-weight-semibold);
}

p {
    margin: 0 0 var(--space-md);
}

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

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

/*
 * One focus style for everything, defined once. :focus-visible rather than :focus so that a
 * mouse click does not leave a ring behind, while a keyboard user always gets one.
 */
:focus-visible {
    outline: var(--focus-ring-width) solid var(--colour-focus);
    outline-offset: var(--focus-ring-offset);
}

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

/*
    THE STEP INDICATOR

    Four numbered circles joined by a line, and below 640px something else entirely: a sentence,
    a bar and the name of the step. Not the same picture shrunk - four circles with labels at
    360px produces four columns of wrapped text, which is less legible than the sentence it
    replaces and takes three times the height.

    Both variants are in the markup and one of them is hidden, rather than one being built by
    JavaScript: the page has to be right before any script runs, and the booking flow is usable
    with none (chapter 21).
*/
.steps {
    list-style: none;
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    margin: 0 0 var(--space-xl);
    padding: 0;
    font-size: var(--font-size-tiny);
    line-height: var(--line-height-tiny);
}

.steps__item {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-xs);
    text-align: center;
    color: var(--colour-text-muted);
}

/*
    The connector: a line from this circle to the next one, drawn behind the circles.

    On the item rather than between items, and not on the last one - a line trailing off the
    right of the final step points at nothing. `translateY(-50%)` from the circle's half-height
    keeps it centred on the circles whatever the circle token becomes.
*/
.steps__item:not(:last-child)::after {
    content: '';
    position: absolute;
    top: calc(var(--layout-step-circle) / 2);
    left: calc(50% + var(--layout-step-circle) / 2);
    right: calc(-50% + var(--layout-step-circle) / 2);
    height: var(--layout-step-line);
    transform: translateY(-50%);
    background: var(--colour-border);
}

.steps__marker {
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--layout-step-circle);
    height: var(--layout-step-circle);
    border-radius: var(--radius-pill);
    background: var(--colour-border);
    color: var(--colour-text-muted);
    font-weight: var(--font-weight-semibold);
    /* Above the connector line, which runs behind it. */
    position: relative;
    z-index: 1;
}

.steps__marker svg {
    width: var(--font-size-body);
    height: var(--font-size-body);
    stroke-width: var(--border-width-thick);
}

.steps__item--done .steps__marker {
    background: var(--colour-accent-surface);
    color: var(--colour-accent);
}

.steps__item--done {
    color: var(--colour-text);
}

.steps__item--current .steps__marker {
    background: var(--colour-accent);
    color: var(--colour-accent-contrast);
}

.steps__item--current {
    color: var(--colour-text);
    font-weight: var(--font-weight-semibold);
}

/* The narrow variant, hidden until its breakpoint. */
.steps-compact {
    display: none;
    margin: 0 0 var(--space-xl);
    font-size: var(--font-size-small);
}

.steps-compact__count {
    color: var(--colour-text-muted);
    font-size: var(--font-size-tiny);
}

.steps-compact__name {
    margin: var(--space-2xs) 0 var(--space-xs);
    font-weight: var(--font-weight-semibold);
}

.steps-compact__track {
    height: var(--layout-progress-bar);
    background: var(--colour-border);
    border-radius: var(--radius-pill);
}

.steps-compact__bar {
    height: 100%;
    background: var(--colour-accent);
    border-radius: inherit;
    transition: width var(--transition-fast);
}

/*
    One class per step rather than an inline width.

    A `style="width: 50%"` would be the obvious way and it is forbidden twice over: the CSP
    carries no inline-style allowance, and DesignTokenDisciplineTest fails any template that
    writes one. Four rules cost four lines and need neither.

    Percentages, not tokens - a fraction of the track is layout behaviour rather than a visual
    decision, which is the same line the token guard draws.
*/
.steps-compact__bar--1-of-4 { width: 25%; }
.steps-compact__bar--2-of-4 { width: 50%; }
.steps-compact__bar--3-of-4 { width: 75%; }
.steps-compact__bar--4-of-4 { width: 100%; }

.steps-compact__bar--1-of-5 { width: 20%; }
.steps-compact__bar--2-of-5 { width: 40%; }
.steps-compact__bar--3-of-5 { width: 60%; }
.steps-compact__bar--4-of-5 { width: 80%; }
.steps-compact__bar--5-of-5 { width: 100%; }

/* --------------------------------------------------------- summary card list */

/*
    A two-column definition list: label left, value right, one row each.
    `grid` rather than a table, because it is not tabular data - it is one thing described.
*/
.summary-card {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--space-xs) var(--space-md);
    margin: 0;
}

.summary-card dt {
    color: var(--colour-text-muted);
    font-size: var(--font-size-small);
}

.summary-card dd {
    margin: 0;
    font-weight: var(--font-weight-medium);
}

/* ------------------------------------------------------------- empty states */

/*
    An empty state is a page that has to say what to do next.

    Not a blank panel and not a bare sentence in grey: a heading, one line of explanation, and a
    way forward that can be pressed. The rule the design asks for - "keine leere Fläche, sondern
    ein Satz, was zu tun ist, und ein Weg dorthin" - is the whole of it.
*/
.empty-state {
    text-align: center;
}

.empty-state__heading {
    margin: 0 0 var(--space-xs);
    font-size: var(--font-size-card-title);
    line-height: var(--line-height-card-title);
}

.empty-state .actions {
    justify-content: center;
    margin-bottom: 0;
}

/*
    The tick on a finished step is drawn, never typed.

    A "✓" is the obvious choice and it is a glyph - and the type stack here is a system stack, so
    on a machine whose fonts lack it the mark renders as nothing at all. That happened once. Every
    mark in this stylesheet is an inline SVG for that reason, including the calendar's arrows.
*/

/* ---------------------------------------------------------------------- form */

.field {
    margin-bottom: var(--space-lg);
}

.field__label {
    display: block;
    margin-bottom: var(--space-2xs);
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-semibold);
}

.field__hint {
    display: block;
    margin-bottom: var(--space-2xs);
    color: var(--colour-text-muted);
    font-size: var(--font-size-sm);
}

.field__error {
    display: block;
    margin-top: var(--space-2xs);
    color: var(--colour-danger);
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-medium);
}

/*
    "Pflichtfeld", as a word.

    A bare asterisk is a convention that has to be learned from a legend somewhere else on the
    page, and a screen reader says "star". The word costs one line and needs no legend.
*/
.field__required {
    color: var(--colour-text-muted);
    font-size: var(--font-size-tiny);
    font-weight: var(--font-weight-regular);
}

/*
    Every text-like control, at one width.

    `input[type="password"]` was missing from this list, so the password field fell back to the
    browser's default size while the address field beside it filled the line. Two fields of two
    widths in the same form reads as an accident, because it was one. Enumerated rather than
    written as `input:not([type="checkbox"])` so that adding a control is a deliberate line.
*/
input[type="text"],
input[type="email"],
input[type="password"],
input[type="tel"],
input[type="number"],
input[type="date"],
input[type="file"],
select,
textarea {
    width: 100%;
    min-height: var(--target-min-size);
    padding: var(--space-xs) var(--control-padding-x);
    font: inherit;
    color: var(--colour-text);
    background: var(--colour-surface);
    /* border-strong rather than border: an input outline carries meaning, so WCAG 1.4.11
       asks it to reach 3:1 rather than merely being visible to somebody looking for it.
       Measured at 3.45:1 on a card and 3.19:1 on the page. */
    border: var(--border-width) solid var(--colour-border-strong);
    border-radius: var(--radius-control);
}

/*
    Focus on a field: the ring, and the border in the accent as well.

    Both, because the ring alone sits *outside* the control and reads as a halo, while the
    border alone is the same 1px line in a different colour and is missed by anybody who is not
    looking for it. The pair is unmistakable and neither replaces the other.
*/
input[type="text"]:focus-visible,
input[type="email"]:focus-visible,
input[type="password"]:focus-visible,
input[type="tel"]:focus-visible,
input[type="number"]:focus-visible,
input[type="date"]:focus-visible,
input[type="file"]:focus-visible,
select:focus-visible,
textarea:focus-visible {
    border-color: var(--colour-accent);
}

textarea {
    min-height: calc(var(--target-min-size) * 3);
    resize: vertical;
}

input[aria-invalid="true"],
select[aria-invalid="true"],
textarea[aria-invalid="true"] {
    border-color: var(--colour-danger);
    border-width: var(--border-width-thick);
}

fieldset {
    margin: 0 0 var(--space-lg);
    padding: var(--space-md);
    border: var(--border-width) solid var(--colour-border);
    border-radius: var(--radius-md);
}

legend {
    padding: 0 var(--space-2xs);
    font-weight: var(--font-weight-medium);
}

.choice {
    display: flex;
    align-items: flex-start;
    gap: var(--space-xs);
    min-height: var(--target-min-size);
    padding: var(--space-2xs) 0;
}

.choice input {
    margin-top: var(--space-2xs);
}

/* -------------------------------------------------------------------- buttons */

.button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2xs);
    min-height: var(--target-min-size);
    padding: var(--space-xs) var(--button-padding-x);
    font: inherit;
    font-size: var(--font-size-body);
    font-weight: var(--font-weight-semibold);
    color: var(--colour-accent-contrast);
    background: var(--colour-accent);
    border: var(--border-width) solid var(--colour-accent);
    border-radius: var(--radius-control);
    text-decoration: none;
    cursor: pointer;
    transition: background var(--transition-fast), border-color var(--transition-fast);
}

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

.button--secondary {
    color: var(--colour-accent);
    background: var(--colour-surface);
    border-color: var(--colour-accent);
}

.button--secondary:hover {
    background: var(--colour-accent-surface);
}

/*
    The quiet half of a pair.

    `--secondary` is an accent-coloured outline: right where two choices are equally real
    ("anrufen" beside "buchen"), wrong beside a primary action, where two saturated buttons
    make the customer decide which is the way forward. This one keeps the shape and drops the
    colour, so a wizard's Zurück reads as available rather than as an alternative.
*/
.button--ghost {
    color: var(--colour-text-muted);
    background: var(--colour-surface);
    border-color: var(--colour-border);
}

.button--ghost:hover {
    color: var(--colour-text);
    background: var(--colour-surface-sunken);
    border-color: var(--colour-border-strong);
}

/*
    A button that is a link in every way but its weight: accent, underlined, no box.

    Underlined rather than coloured only - WCAG 1.4.1 again, and inside a paragraph the colour
    is the only other thing distinguishing it from the sentence around it.
*/
.button--text {
    min-height: var(--target-min-size);
    padding: var(--space-xs) 0;
    color: var(--colour-accent);
    background: none;
    border-color: transparent;
    text-decoration: underline;
}

.button--text:hover {
    background: none;
    color: var(--colour-accent-hover);
}

.button[disabled] {
    /* Dimmed through the muted text token rather than through opacity: opacity below 1 drags
       the contrast ratio down by an amount nobody has checked. */
    color: var(--colour-text-muted);
    background: var(--colour-surface-sunken);
    border-color: var(--colour-border);
    cursor: not-allowed;
}

/*
    A row of buttons, wrapping.

    Two gaps rather than one. With a single value the wrapped rows sit closer together than the
    buttons within a row do, because a button is wide and short: the same measurement reads as
    generous beside a 3rem-wide label and cramped under a 2.75rem-tall one. The row gap is one
    step up the scale, which makes the two read as equal.

    No margin below, on purpose - the space under a button row belongs to whatever follows it,
    and that is the `* + h1` rule above.
*/
.actions {
    display: flex;
    flex-wrap: wrap;
    column-gap: var(--space-sm);
    row-gap: var(--space-md);
    margin-top: var(--space-xl);
}

/*
    The wizard's back/forward pair: back on the left edge, the primary action on the right
    (docs/adr/0014-wizard-navigation.md). `space-between` rather than margins, so the two stay
    pinned to their edges whatever their widths are. The narrow-screen stacking - primary on
    top, back below - is inherited from the shared `.actions` rule further down.
*/
.actions--wizard {
    justify-content: space-between;
    align-items: center;
}

.actions--wizard .button {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
}

/*
    Every icon inside a button, sized once.

    An inline `<svg>` with a viewBox and no width or height has no intrinsic size: its box is
    whatever layout gives it. In a wide flex row that is "all of it" - a plus sign the size of
    the whole drop area, which is exactly what shipped - and in a narrow one it is nothing at
    all. Both from the same missing pair of numbers. So the pair lives here, once, for every
    button-shaped control rather than per place somebody remembered.
*/
.button svg,
.file-card__remove svg,
.file-list__remove svg {
    flex: none;
    width: var(--layout-icon);
    height: var(--layout-icon);
    stroke-width: var(--layout-icon-stroke);
}

/*
    A page that is only a form: signing in, asking for a link, setting a password.

    Bounded and centred rather than stretched across the window. Nothing else is on these pages,
    so full width puts a two-field form alone at the top left of an empty screen with its labels
    far from its fields. The width is a token because it is a design decision, not a number that
    happened to look right.

    Intrinsic, so there is no media query: `max-width` yields to a narrow screen on its own and
    the padding is the only thing a phone gives up.
*/
.sign-in {
    max-width: var(--layout-form-width);
    margin-inline: auto;
    padding: var(--space-xl);
    background: var(--colour-surface);
    border: var(--border-width) solid var(--colour-border);
    border-radius: var(--radius-md);
}

/* The first heading in the card sets the tone; nothing above it to keep clear of. */
.sign-in > h1 {
    margin-bottom: var(--space-lg);
}

/* The last control should not push against the card's edge from the inside. */
.sign-in .actions {
    margin-bottom: 0;
}

/*
    The way out of the card - "Passwort vergessen?", "Zurück zur Anmeldung".

    Set apart from the button above it rather than tucked under it. A link directly beneath a
    submit button reads as a second, quieter button, and this one does the opposite thing.
*/
.sign-in__aside {
    margin: var(--space-lg) 0 0;
    font-size: var(--font-size-sm);
}

/* A quiet second line under a card title - an address, a code, a date. */
.card__meta {
    margin: calc(var(--space-sm) * -1) 0 var(--space-md);
    color: var(--colour-text-muted);
    font-size: var(--font-size-sm);
}

/* ------------------------------------------------------------------- messages */

.message {
    margin: 0 0 var(--space-lg);
    padding: var(--space-md);
    border-left: var(--border-width-thick) solid currentColor;
    border-radius: var(--radius-sm);
}

.message__title {
    margin: 0 0 var(--space-2xs);
    font-size: var(--font-size-base);
    font-weight: var(--font-weight-bold);
}

.message--info {
    color: var(--colour-info);
    background: var(--colour-info-surface);
}

.message--success {
    color: var(--colour-success);
    background: var(--colour-success-surface);
}

.message--warning {
    color: var(--colour-warning);
    background: var(--colour-warning-surface);
}

.message--danger {
    color: var(--colour-danger);
    background: var(--colour-danger-surface);
}

.message p:last-child,
.message ul:last-child {
    margin-bottom: 0;
}

/* ---------------------------------------------------------------------- cards */

.card {
    padding: var(--space-md);
    background: var(--colour-surface-raised);
    border: var(--border-width) solid var(--colour-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
}

/*
    Cards that stack in normal flow separate themselves. Nothing else is doing it for them:
    an admin page is a run of sibling `<div class="card">` with no container between.
*/
.card + .card {
    margin-top: var(--space-md);
}

/*
    ...and never where a container already has a gap.
    THIS COST A ROW OF TILES.

    The rule above is an adjacent-sibling rule, so it skips the first card and applies to the
    rest. In a stack that is exactly right. In the service grid it meant every tile but the first
    carried a 16px top margin, measured from its grid cell's edge - so the first tile sat 16px
    higher than its neighbours and lined up with them neither at the top nor at the bottom, while
    the markup, the classes and the element were identical in every card.

    Spacing between items belongs to whichever container places them. Where that container has a
    `gap`, an item margin is not a smaller version of the same idea - it is a second, competing
    one, and the two disagree at exactly one item.
*/
.service-list > .card {
    margin: 0;
}

/*
    A card's title and what follows it.

    The old quarter-step below was sized for a title with a code beside it and nothing else. Where
    a card holds a form - the catalogue, people, access - it put the first label directly under the
    heading, so the label read as part of the title. One step below a heading, everywhere, is the
    same decision the `* + h1` rule makes above it; the meta line pulls back up because it belongs
    to the title rather than to what comes after.
*/
.card__title {
    margin: 0 0 var(--space-md);
    font-size: var(--font-size-lg);
}

.service-list {
    display: grid;
    gap: var(--space-md);
    padding: 0;
    margin: 0;
    list-style: none;
    /*
        `auto-fill` with a minimum width gives the single column on a narrow screen for free -
        below one card's minimum there is room for exactly one track. No media query, and
        therefore nothing to keep in step with the breakpoints elsewhere.
    */
    grid-template-columns: repeat(auto-fill, minmax(var(--layout-card-min), 1fr));
}

/*
    The gap belongs to the container and to nothing else.

    280px tracks with a 20px gap give exactly the column counts the design asks for, measured at
    the widths the browser test drives: one below 640, two at 768, three at 1280. No media query,
    and therefore nothing to keep in step with the breakpoints elsewhere.
*/
.service-list {
    gap: var(--layout-grid-gap);
}

/*
    THE SERVICE CARD

    Equal height per row, the whole card clickable, and the title is a heading rather than a
    link.

    The height comes from the grid: tracks in a row are as tall as the tallest item, and the card
    is a column flexbox whose meta line is pushed down by `margin-top: auto`. So a two-line
    description and a five-line one produce cards of the same height with their duration on the
    same baseline - which is the thing that makes a row scannable.

    The click area comes from one link stretched over the card by a pseudo-element. One link, not
    several: a card wrapped in an anchor would read its whole content as the link text, and a
    second "mehr" link would put the same destination in the tab order twice. The heading stays a
    heading, so "list the headings" still describes this page.
*/
.service-card {
    position: relative;
    display: flex;
    flex-direction: column;
    padding: var(--space-card-padding);
    border-radius: var(--radius-card);
    /* Lift and border both animate; nothing moves on load, because a transition only runs on a
       change of state and there is none at first paint. */
    transition:
        border-color var(--transition-fast),
        box-shadow var(--transition-fast),
        transform var(--transition-fast),
        background-color var(--transition-fast);
}

/*
    THE ICON FIELD

    A square of the accent wash with a 20px stroke drawing in it. Fixed size in both directions
    and `flex: none`, or a tall description would stretch it into a rectangle - a flex item's
    cross size follows its container unless it is told otherwise.
*/
.service-icon {
    display: flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: var(--layout-icon-field);
    height: var(--layout-icon-field);
    border-radius: var(--radius-card);
    background: var(--colour-accent-surface);
    color: var(--colour-accent);
}

.service-icon__glyph {
    width: var(--layout-icon);
    height: var(--layout-icon);
    stroke-width: var(--layout-icon-stroke);
}

.service-card__title {
    margin: var(--space-md) 0 0;
    font-size: var(--font-size-card-title);
    line-height: var(--line-height-card-title);
    font-weight: var(--font-weight-semibold);
}

/*
    Two lines, then an ellipsis.

    `-webkit-line-clamp` is the only thing that truncates by line rather than by character, and it
    is supported everywhere despite the prefix - it is in the standard as `line-clamp`, which is
    set alongside so this stops needing the prefix without another edit. The `overflow: hidden` is
    what actually clips; the clamp only decides where.
*/
.service-card__description {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    margin: var(--space-xs) 0 0;
    color: var(--colour-text-muted);
    font-size: var(--font-size-small);
    line-height: var(--line-height-small);
}

.service-card__link {
    color: inherit;
    text-decoration: none;
}

.service-card__link::after {
    /* The click target: the whole card. */
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
}

/*
    The card's foot: the duration, and the mark underneath it.

    Two rows, and the second one exists whether or not there is a mark. That is the whole point.

    It used to be one wrapping flex line with the tag trailing the duration, and at this card
    width the two did not fit: the tag wrapped, the block grew from 36px to 71px, and because it
    is pushed to the bottom the duration jumped 35px up - in exactly those cards that happen to be
    on-site services. So "Dauer etwa 60 Minuten" sat on a different baseline from card to card,
    and whether it did depended on how wide the browser was.

    A reserved row cannot do that. The duration is always the first row and the block is always
    the same height, so the baseline is the same in every card, with a mark or without one.
    Unoccupied, the row is invisible - it sits at the bottom of the card and reads as padding.
*/
.service-card__meta {
    display: grid;
    grid-template-rows: auto var(--layout-tag-row);
    justify-items: stretch;
    align-content: end;
    gap: var(--space-2xs);
    /* Pushed to the bottom, which is what makes equal-height cards line their meta up. */
    margin-top: auto;
    padding-top: var(--space-md);
    color: var(--colour-text-muted);
    font-size: var(--font-size-tiny);
    line-height: var(--line-height-tiny);
}

/* Row one: price on the left, duration on the right, on one baseline. */
.service-card__figures {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-sm);
}

.service-card__price {
    color: var(--colour-text);
    font-size: var(--font-size-price);
    font-weight: var(--font-weight-bold);
    font-variant-numeric: tabular-nums;
}

/* Row two, always present, occupied or not - see the note above. */
.service-card__marks {
    display: flex;
    align-items: center;
    gap: var(--space-2xs);
    min-height: var(--layout-tag-row);
}

.service-card:hover {
    border-color: var(--colour-accent);
    box-shadow: var(--shadow-md);
    transform: translateY(var(--lift-hover));
}

/*
    Focus is not hover. A keyboard user gets the ring *and* the same lift, because a card that
    only responded to a mouse would leave them guessing where they are (WCAG 2.4.7, 2.4.11).
*/
.service-card:focus-within {
    /* The ring, and the lift - but not the accent border. The specification is explicit that
       focus leaves the border alone, so that focus and selection stay two different pictures. */
    box-shadow: var(--shadow-md);
    outline: var(--focus-ring-width) solid var(--colour-focus);
    outline-offset: var(--focus-ring-offset);
}

/*
    CHOSEN, AND NEVER BY COLOUR ALONE

    A 2px accent border, the accent wash behind it, and a drawn tick in the corner. Three
    carriers for one fact, because WCAG 1.4.1 does not accept a fill as information and a
    red-green colour deficiency turns a wash into no signal at all.

    The border replaces the 1px one rather than sitting inside it, so the tile does not change
    size when it is chosen - a 1px growth would nudge every neighbour in the row.
*/
.service-card--chosen {
    border-width: var(--border-width-thick);
    border-color: var(--colour-accent);
    background: var(--colour-accent-surface);
}

.service-card__chosen-mark {
    position: absolute;
    top: var(--space-sm);
    right: var(--space-sm);
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--layout-icon);
    height: var(--layout-icon);
    color: var(--colour-accent);
    /* Above the stretched link, or the tick would be inside the click target's stacking context
       and disappear under it. */
    z-index: 1;
}

.service-card__chosen-mark svg {
    width: 100%;
    height: 100%;
    stroke-width: var(--border-width-thick);
}

/* The ring belongs to the card, drawn above; a second one on the link inside would double it. */
.service-card__link:focus-visible {
    outline: none;
}

/*
    A small mark, not a sentence. "Vor-Ort-Leistung" in running text competes with the
    description for the same attention; as a tag it is scanned instead of read.
*/
.tag {
    display: inline-flex;
    align-items: center;
    /* The same token the reserved row uses, so the row and its occupant cannot disagree. */
    min-height: var(--layout-tag-row);
    padding: var(--badge-padding-y) var(--badge-padding-x);
    border-radius: var(--radius-pill);
    /* Info rather than the accent: the accent means "you can act on this", and a badge is a
       fact about the service rather than a thing to press. 6.60:1 as a pair. */
    background: var(--colour-info-surface);
    color: var(--colour-info);
    font-size: var(--font-size-tiny);
    line-height: var(--line-height-tiny);
    font-weight: var(--font-weight-medium);
    /* Above the stretched link, or it would swallow the tag's own hover feedback. */
    position: relative;
}

/* ---------------------------------------------------------------------- slots */

.slot-group {
    margin-bottom: var(--space-xl);
}

.slot-group__title {
    margin-bottom: var(--space-xs);
}

.slot-list {
    display: grid;
    gap: var(--space-xs);
    padding: 0;
    margin: 0;
    list-style: none;
    grid-template-columns: repeat(auto-fill, minmax(var(--layout-slot-min), 1fr));
}

.slot {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    min-height: var(--target-min-size);
    padding: var(--space-xs) var(--space-sm);
    background: var(--colour-surface);
    border: var(--border-width) solid var(--colour-border-strong);
    border-radius: var(--radius-sm);
}

.slot:has(input:checked) {
    border-color: var(--colour-accent);
    border-width: var(--border-width-thick);
    background: var(--colour-info-surface);
}

/* ------------------------------------------------------------- slot calendar */

/*
    THE TWO-STAGE PICKER

    A month grid, then the times of the chosen day. The point of it is that the date is written
    once - as the month heading and again over the times - instead of on every one of two hundred
    options.
*/
.slot-calendar {
    margin-bottom: var(--space-xl);
}

.slot-calendar__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    margin-bottom: var(--space-sm);
}

.slot-calendar__month {
    margin: 0;
    font-size: var(--font-size-lg);
    text-align: center;
}

.slot-calendar__grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: var(--space-2xs);
}

.slot-calendar__weekday {
    padding-bottom: var(--space-2xs);
    color: var(--colour-text-muted);
    font-size: var(--font-size-xs);
    text-align: center;
}

.slot-calendar__blank {
    display: block;
}

/*
    A day with nothing free is drawn and disabled rather than left out. An absent day reads as a
    hole in the month; a dimmed one says the diary was checked and there is nothing - which is
    the answer somebody came for.

    It is still `--colour-text-muted` on the page, so it stays legible: "not available" must not
    mean "not readable".
*/
.slot-calendar__day {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    min-height: var(--target-min-size);
    padding: var(--space-2xs);
    background: var(--colour-surface);
    border: var(--border-width) solid var(--colour-border);
    border-radius: var(--radius-sm);
    color: var(--colour-text-muted);
    font: inherit;
    font-size: var(--font-size-sm);
}

.slot-calendar__day:disabled {
    cursor: default;
}

/* A day with something free: the accent wash, the figure in the accent, and a dot beneath it -
   three carriers again, because a wash alone is colour alone. */
/* The arrow beside a pager's word: drawn, never a glyph. */
.button__arrow {
    width: var(--font-size-body);
    height: var(--font-size-body);
    stroke-width: var(--layout-icon-stroke);
    fill: none;
    stroke: currentColor;
    flex: none;
}

/*
    A day cell holds a figure and, under it, a dot. Column layout with the dot last, so the
    figure stays optically centred whether or not the dot is there - the cell reserves the dot's
    row either way.
*/
.slot-calendar__day {
    flex-direction: column;
    gap: var(--space-3xs);
}

.slot-calendar__dot {
    width: var(--space-2xs);
    height: var(--space-2xs);
    border-radius: var(--radius-pill);
    background: currentColor;
}

/* A day with something free: the accent wash, the figure in the accent, and a dot beneath it -
   three carriers again, because a wash alone is colour alone. */
.slot-calendar__day--free {
    border-color: var(--colour-border-strong);
    color: var(--colour-accent);
    font-weight: var(--font-weight-medium);
    cursor: pointer;
}

.slot-calendar__day--free:not(.slot-calendar__day--chosen):hover {
    border-color: var(--colour-accent);
    background: var(--colour-accent-surface);
}

/*
    Chosen: filled, and `aria-pressed` says the same thing where a fill cannot be seen.

    The hover rule above excludes the chosen day on purpose. Without that exclusion it wins on
    specificity and paints its pale wash under the white text of the chosen state - which is
    exactly what a mouse leaves behind after a click, so the day somebody just picked is the one
    they cannot read.
*/
.slot-calendar__day--chosen {
    background: var(--colour-accent);
    border-color: var(--colour-accent);
    color: var(--colour-accent-contrast);
    font-weight: var(--font-weight-bold);
}

.slot-calendar__times {
    margin-top: var(--space-lg);
}

.slot-calendar__times h4 {
    margin: 0 0 var(--space-sm);
    font-size: var(--font-size-base);
}

/* Not a blank area - see the template. */
.slot-calendar__empty {
    margin-top: var(--space-md);
    color: var(--colour-text-muted);
}

/*
    The times: compact, wrapping, no date on any of them. Each button carries the full date in
    its `aria-label`, because a screen reader that lands on "09:15" with no heading in earshot
    learns nothing.
*/
.slot-times {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs);
    padding: 0;
    margin: 0;
    list-style: none;
}

/* A chip: 44 high, 16 either side, wrapping grid with an 8 gap - the gap lives on the list. */
.slot-times__time {
    min-height: var(--target-min-size);
    min-width: var(--layout-slot-min);
    padding: var(--space-xs) var(--space-sm);
    background: var(--colour-surface);
    border: var(--border-width) solid var(--colour-border-strong);
    border-radius: var(--radius-sm);
    color: var(--colour-text);
    font: inherit;
    cursor: pointer;
}

.slot-times__time:not(.slot-times__time--chosen):hover {
    border-color: var(--colour-accent);
    background: var(--colour-accent-surface);
}

.slot-times__time--chosen {
    background: var(--colour-accent);
    border-color: var(--colour-accent);
    color: var(--colour-accent-contrast);
    font-weight: var(--font-weight-bold);
}

.slot-day__title {
    margin: var(--space-lg) 0 var(--space-xs);
    font-size: var(--font-size-base);
}

/*
    THERE IS NO BREAKPOINT HERE, AND THAT IS DELIBERATE

    On a telephone the calendar already takes the full width and the times already sit below it:
    the grid is seven equal fractions of whatever it is given, and the times wrap. Both adapt
    because of how they are built, not because a width was measured.

    This stylesheet has no media queries at all, and adding the first one here would be the wrong
    trade. A breakpoint is a number that has to live outside the token file - custom properties
    do not work in `@media` - so every one of them is an exception to the rule that all sizes are
    tokens, and an exception granted for something the layout does by itself is the worst kind.

    What a phone actually needs is a thumb-sized target, and that is `--target-min-size` on the
    day cell and the time button: 44px, from WCAG 2.5.5, at every width.
*/

/* ----------------------------------------------------------------- price table */

.summary {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: var(--space-lg);
}

.summary th,
.summary td {
    padding: var(--space-xs) var(--space-2xs);
    text-align: left;
    border-bottom: var(--border-width) solid var(--colour-border);
}

.summary td:last-child,
.summary th:last-child {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.summary__total td,
.summary__total th {
    font-weight: var(--font-weight-bold);
    border-bottom: none;
    border-top: var(--border-width-thick) solid var(--colour-border-strong);
}

/* ----------------------------------------------------------------- contact card

    The way forward when the portal cannot take the booking: a postcode we do not drive to.

    Deliberately not a `.message--danger` box. The customer has done nothing wrong, and the
    van's radius is not their mistake - an alarm-coloured panel would tell them it was. So it
    is an ordinary card with a heading, two sentences and the two ways to reach a person, and
    the phone is the primary action because a conversation is what actually solves this case.
*/
.contact-card__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs) var(--space-sm);
    margin-top: var(--space-md);
}

/* The number and the address beside their buttons: readable, and copyable by hand for
   somebody reading this on a desktop with no telephone attached to it. */
.contact-card__detail {
    font-size: var(--font-size-small);
    color: var(--colour-text-muted);
}

.contact-card__aside {
    margin: var(--space-lg) 0 0;
    font-size: var(--font-size-small);
    color: var(--colour-text-muted);
}

/* ------------------------------------------------------------------- file list */

.file-list {
    padding: 0;
    margin: 0 0 var(--space-md);
    list-style: none;
}

.file-list__item {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs);
    padding: var(--space-xs) 0;
    border-bottom: var(--border-width) solid var(--colour-border);
}

/* ------------------------------------------------------------------- uploading

    The optional half of the Anliegen step: a drop area, and a card per file that arrived.

    Deliberately quieter than the textarea above it. The description is what the booking is
    about; a bordered, filled panel here would out-shout it and turn an aid into the task. So
    the drop area is a dashed outline on the sunken surface - present, obviously interactive,
    and visibly not the main event.
*/

.upload {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: var(--space-xs) var(--space-sm);
    min-height: var(--layout-dropzone);
    padding: var(--space-sm) var(--space-md);
    text-align: center;
    background: var(--colour-surface-sunken);
    border: var(--border-width-thick) dashed var(--colour-border-strong);
    border-radius: var(--radius-card);
    transition: background var(--transition-fast), border-color var(--transition-fast);
}

/* Only while something is actually over it - the script adds this. */
.upload--dragging {
    background: var(--colour-accent-surface);
    border-color: var(--colour-accent);
}

/*
    "Datei hierher ziehen", with the arrow that says so.

    Named before the chooser and carrying its own icon, because dropping is the thing a drop
    area has to advertise: a dashed rectangle alone is a convention only some people read, and
    the button beside it is the fallback everybody understands.
*/
.upload__hint {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    margin: 0;
    font-size: var(--font-size-small);
    color: var(--colour-text-muted);
}

.upload__hint svg {
    flex: none;
    width: var(--layout-icon);
    height: var(--layout-icon);
    stroke-width: var(--layout-icon-stroke);
}

/* The limits belong under the row, not in it: they are read once and then ignored. */
.upload__limits {
    flex-basis: 100%;
}

.upload__limits {
    margin: 0;
    font-size: var(--font-size-tiny);
    color: var(--colour-text-muted);
}

/*
    The chooser.

    A file input cannot be styled, so the real one is taken out of the layout and the label in
    front of it does the looking - which is also why the label is a `.button`: one button style
    in the portal, not a second one that merely resembles it. The input keeps its own focus
    ring by handing it to the label, so a keyboard reaches this like any other control.
*/
input.upload__input {
    position: absolute;
    width: var(--border-width);
    height: var(--border-width);
    padding: 0;
    margin: calc(var(--border-width) * -1);
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

input.upload__input:focus-visible + .upload__choose {
    outline: var(--focus-ring-width) solid var(--colour-focus);
    outline-offset: var(--focus-ring-offset);
}

/*
    The label the customer actually clicks.

    `flex: none` and `nowrap` because it is a flex item in the column above: without them the
    item shrinks to the column width the browser happens to compute and breaks "Datei
    auswählen" across two lines beside its own icon.
*/
.upload__choose {
    flex: none;
    white-space: nowrap;
    cursor: pointer;
}

/* ------------------------------------------------------------------ file cards */

.file-cards {
    display: grid;
    gap: var(--space-xs);
    padding: 0;
    margin: 0 0 var(--space-md);
    list-style: none;
}

.file-card {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-xs) var(--space-sm);
    background: var(--colour-surface);
    border: var(--border-width) solid var(--colour-border);
    border-radius: var(--radius-control);
}

.file-card__preview {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--layout-thumb);
    height: var(--layout-thumb);
    overflow: hidden;
    background: var(--colour-surface-sunken);
    border: var(--border-width) solid var(--colour-border);
    border-radius: var(--radius-sm);
    font-size: var(--font-size-tiny);
    font-weight: var(--font-weight-bold);
    color: var(--colour-text-muted);
}

.file-card__thumb {
    width: 100%;
    height: 100%;
    /* Cropped rather than letterboxed: a row of equal squares reads faster. */
    object-fit: cover;
}

.file-card__body {
    /* Without this a long file name refuses to shrink and pushes the × off the card. */
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-3xs);
}

.file-card__name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--font-size-small);
}

.file-card__meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-xs);
    font-size: var(--font-size-tiny);
    color: var(--colour-text-muted);
}

.file-list__size {
    font-size: var(--font-size-small);
    color: var(--colour-text-muted);
}

/*
    The removal control at the right edge of every uploaded file.

    A real button, full touch-target size, pushed to the edge with the auto margin so the
    row reads name-size-verdict on the left and the one destructive act on the right. The
    hover and focus states make it unmistakably interactive - an × that only looks like
    decoration is a file nobody realises they can take back.
*/
/*
    Taking a file back out.

    A bare × in a borderless box was read as decoration - it was reported as "unklar" by the
    first person who met it. So it looks like what it is: a bordered control with the word on
    it. The word is what makes it unambiguous; the icon only makes it quick to find.
*/
.file-card__remove,
.file-list__remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2xs);
    min-height: var(--target-min-size);
    margin-left: auto;
    padding: var(--space-2xs) var(--space-xs);
    font: inherit;
    font-size: var(--font-size-small);
    background: var(--colour-surface);
    border: var(--border-width) solid var(--colour-border-strong);
    border-radius: var(--radius-control);
    color: var(--colour-text-muted);
    white-space: nowrap;
    cursor: pointer;
}


.file-card__remove:hover,
.file-list__remove:hover {
    background: var(--colour-danger-surface);
    border-color: var(--colour-danger);
    color: var(--colour-danger);
}

.file-card__remove:focus-visible,
.file-list__remove:focus-visible {
    outline: var(--focus-ring-width) solid var(--colour-focus);
    outline-offset: var(--focus-ring-offset);
}

.file-card__remove[disabled],
.file-list__remove[disabled] {
    cursor: default;
    color: var(--colour-border-strong);
    background: none;
}

/* ------------------------------------------------------------------ attachments

    Files on a case, as tiles rather than as a list of names.

    A workshop case is mostly photographs, and a photograph's file name says nothing about what
    it shows. The grid answers "which one do I want" at a glance; the name underneath answers
    "which one is it" once that question comes up.

    Everything here degrades: the tiles are links, so without the lightbox script a click opens
    the file, and the upload is an ordinary multiple-file input whether or not anything can be
    dragged onto it.
*/

.attachments {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(var(--layout-tile), 1fr));
    gap: var(--space-md);
    padding: 0;
    margin: 0 0 var(--space-lg);
    list-style: none;
}

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

.attachments__tile {
    display: flex;
    align-items: center;
    justify-content: center;
    /* Square, so a portrait photograph and a landscape one make the same shape of tile and the
       grid does not develop ragged rows. */
    aspect-ratio: 1;
    overflow: hidden;
    background: var(--colour-surface-sunken);
    border: var(--border-width) solid var(--colour-border);
    border-radius: var(--radius-control);
    text-decoration: none;
}

.attachments__tile:hover {
    border-color: var(--colour-border-strong);
}

.attachments__thumb {
    width: 100%;
    height: 100%;
    /* Cropped rather than letterboxed: a grid of consistent squares reads faster than one of
       centred images floating in grey. The full frame is one click away in the lightbox. */
    object-fit: cover;
}

.attachments__tile--document,
.attachments__tile--refused {
    flex-direction: column;
    gap: var(--space-2xs);
    padding: var(--space-xs);
    text-align: center;
}

.attachments__tile--refused {
    border-style: dashed;
    color: var(--colour-text-muted);
}

.attachments__kind {
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-bold);
    color: var(--colour-text-muted);
}

.attachments__open {
    font-size: var(--font-size-tiny);
    color: var(--colour-accent);
}

.attachments__name {
    /* One line, cut with an ellipsis. The full name is in the title attribute and in the
       lightbox caption; a wrapped four-line file name would push the tiles apart. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--font-size-small);
}

.attachments__meta {
    font-size: var(--font-size-tiny);
    color: var(--colour-text-muted);
}

.attachments__delete {
    margin: 0;
}

.attachments__upload {
    padding-top: var(--space-md);
    border-top: var(--border-width) solid var(--colour-border);
}

/* Only while something is actually being dragged over the form - the script adds it. */
.attachments__upload--dragging {
    border: var(--border-width-thick) dashed var(--colour-accent);
    border-radius: var(--radius-card);
    background: var(--colour-accent-surface);
}

.attachments__droptext {
    margin: var(--space-2xs) 0 0;
    font-size: var(--font-size-small);
    color: var(--colour-text-muted);
}

/* --------------------------------------------------------------------- lightbox

    Built rather than pulled in: what it has to do is show one image, move to the next, and
    close - and the accessible version of that is shorter than the configuration of a library
    that also does video, zoom and captions.
*/

.lightbox {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-md);
    padding: var(--space-lg);
    background: var(--colour-scrim);
}

.lightbox[hidden] {
    display: none;
}

.lightbox__image {
    max-width: 100%;
    /* Room for the caption and the controls underneath without the image ever scrolling. */
    max-height: 75vh;
    object-fit: contain;
}

.lightbox__caption {
    color: var(--colour-text-inverse);
    font-size: var(--font-size-small);
    text-align: center;
}

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

.lightbox__button {
    min-width: var(--target-min-size);
    min-height: var(--target-min-size);
    padding: var(--space-xs) var(--space-md);
    background: var(--colour-surface);
    color: var(--colour-text);
    border: var(--border-width) solid var(--colour-border-strong);
    border-radius: var(--radius-control);
    font-size: var(--font-size-body);
    cursor: pointer;
}

.lightbox__button[disabled] {
    opacity: 0.4;
    cursor: default;
}

.lightbox__close {
    position: absolute;
    top: var(--space-md);
    right: var(--space-md);
}

.badge {
    display: inline-block;
    padding: var(--space-3xs) var(--space-xs);
    border-radius: var(--radius-pill);
    font-size: var(--font-size-xs);
    font-weight: var(--font-weight-medium);
}

.badge--pending {
    color: var(--colour-warning);
    background: var(--colour-warning-surface);
}

.badge--clean {
    color: var(--colour-success);
    background: var(--colour-success-surface);
}

.badge--rejected {
    color: var(--colour-danger);
    background: var(--colour-danger-surface);
}

/*
 * Alpine sets x-cloak on elements that must not appear before it has taken over. Without
 * this rule the no-JavaScript markup flashes on every load for people who do have it.
 */
[x-cloak] {
    display: none !important;
}

/*
 * The counterpart: markup that exists only for browsers without JavaScript. Alpine removes
 * the attribute from <html> as soon as it starts, so these disappear for everybody else.
 * Written this way round on purpose - with JavaScript off, nothing has to run for the page
 * to be complete.
 */
html.has-js .no-js-only {
    display: none;
}

.has-js-only {
    display: none;
}

html.has-js .has-js-only {
    display: block;
}

/*
    `display: block` is right for a paragraph and wrong for a control: it flattens the
    inline-flex a button needs to centre its icon. Marking the element as inline restores it
    without weakening the utility for everything else.
*/
html.has-js .has-js-only.is-inline {
    display: inline-flex;
}

/*
 * Text for screen readers and nothing else. Clipped rather than `display: none`, which would
 * remove it from the accessibility tree as well and defeat the purpose - this is how "required"
 * gets announced without a red asterisk having to mean something to somebody who cannot see it.
 */
.visually-hidden {
    position: absolute;
    width: var(--border-width);
    height: var(--border-width);
    margin: calc(var(--border-width) * -1);
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ------------------------------------------------------------------ dispatch calendar */

/*
 * The React island's styles live here rather than in the bundle, so the calendar is made of the
 * same tokens as everything else. When SNOGARD decides what the portal looks like (Q-35), one
 * file changes - and the calendar changes with it instead of keeping a palette of its own.
 */

.calendar__grid {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
}

.calendar__grid th,
.calendar__grid td {
    border: var(--border-width) solid var(--colour-border);
    padding: var(--space-3xs);
    vertical-align: top;
    font-size: var(--font-size-sm);
}

.calendar__cell {
    height: var(--layout-calendar-row);
    background: var(--colour-surface-sunken);
}

/* Released capacity is the only capacity there is (chapter 6), so an unreleased cell reads as
   what it is: not part of the working week. */
.calendar__cell--released {
    background: var(--colour-surface);
}

.calendar__cell--blocked {
    /* Blocked is visibly different from unreleased: something was released here and somebody
       withdrew it. The appointments already inside stay visible (Q-28). */
    background: var(--colour-warning-surface);
}

.calendar__cell--target {
    outline: var(--border-width-thick) dashed var(--colour-accent);
    outline-offset: calc(var(--border-width) * -2);
}

.calendar__appointment {
    display: flex;
    flex-direction: column;
    gap: var(--space-3xs);
    margin-bottom: var(--space-3xs);
    padding: var(--space-2xs);
    border-radius: var(--radius-sm);
    border-left: var(--border-width-thick) solid currentColor;
    background: var(--colour-surface-raised);
    cursor: grab;
}

/* Not draggable, and it says so rather than silently ignoring the drag: the deadline has passed
   or the service does not allow it. */
.calendar__appointment--fixed {
    cursor: not-allowed;
    opacity: 1;
    border-left-style: dotted;
}

.calendar__window-toggle {
    float: right;
    min-height: auto;
    padding: 0 var(--space-3xs);
    font: inherit;
    background: none;
    border: none;
    cursor: pointer;
}

.calendar__legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
    list-style: none;
    padding: 0;
    margin-top: var(--space-md);
    font-size: var(--font-size-sm);
}

.calendar__legend-item {
    padding-left: var(--space-xs);
    border-left: var(--border-width-thick) solid currentColor;
}

/*
 * Six colours for the capacity pools of chapter 6 - telephone, remote, workshop, production,
 * counter, field. Each is a token, so the palette is still one file.
 *
 * Colour is never the only carrier: every appointment also names its pool in the legend and its
 * service in the card, because a dispatcher with colour vision deficiency has to read the same
 * calendar (chapter 21, WCAG 1.4.1).
 */
.calendar__pool--0 { color: var(--colour-pool-0); }
.calendar__pool--1 { color: var(--colour-pool-1); }
.calendar__pool--2 { color: var(--colour-pool-2); }
.calendar__pool--3 { color: var(--colour-pool-3); }
.calendar__pool--4 { color: var(--colour-pool-4); }
.calendar__pool--5 { color: var(--colour-pool-5); }


/* ------------------------------------------------------------- the narrow end */

/*
    THE ONLY MEDIA QUERIES IN THIS FILE

    A breakpoint cannot be a custom property - a media query prelude is evaluated before custom
    properties exist, in every browser - so every one of these is a literal outside tokens.css,
    which is the rule this project otherwise holds. They are here because three things genuinely do
    not adapt by themselves: a four-circle step indicator, a pair of buttons that must stack, and
    a page title that is too large for a 360px screen.

    What keeps the exception from becoming a hole is
    DesignTokenDisciplineTest::testEveryBreakpointIsOneTheTokenFileDeclares - a width used here
    must be one the token file declares as a --breakpoint-*, or one less for the max-width side.
    The type scale and the gutter are not here at all: they are token overrides and live in
    tokens.css, where they belong.
*/
@media (max-width: 639px) {
    /* Four circles become a sentence, a bar and a name. */
    .steps {
        display: none;
    }

    .steps-compact {
        display: block;
    }

    /*
        Full width, stacked, primary first.

        `column-reverse` rather than reordering the markup: the primary action belongs last in
        the DOM on a wide screen, where it sits on the right and reads as the end of the row.
        Moving it in the markup for the narrow case would move it for the keyboard too, and tab
        order should follow the page rather than the paint.
    */
    .actions {
        flex-direction: column-reverse;
        align-items: stretch;
    }

    .actions .button {
        width: 100%;
    }
}

/*
    Nothing moves for somebody who asked for less motion.

    The duration token is already zero by then, which stops the transition - this stops the
    end state as well, because a lift that arrives in 0ms is still a jump.
*/
@media (prefers-reduced-motion: reduce) {
    .service-card:hover {
        transform: none;
    }
}
