/*
 * One block per entry in docs/ux/components.md. ADR 0028 §2.
 *
 * Only the components an MVP screen needs are here — issue #43 puts the rest
 * out of scope. Each block is written against the markup contract recorded
 * beside its entry in components.md; if you change the element or the role, the
 * contract moves with it.
 *
 * Auth card, Form field, Form error summary and .button--block are the
 * exception to "issue #43 ships the rest": the breeder sign-in screen
 * (issue #41) needs them and shipped first, against this same token scale.
 * They live here rather than in a screen-specific sheet because every other
 * built component does.
 *
 * No colour value may appear in this file. Reference tokens with var(--…).
 */

@layer components {
    /* ---------------------------------------------------------------- icon */

    .icon {
        inline-size: var(--space-5);
        block-size: var(--space-5);
        flex: 0 0 auto;
        fill: none;
        stroke: currentColor;
        stroke-width: 2;
        stroke-linecap: round;
        stroke-linejoin: round;
    }

    /*
     * A 40px rounded-square tile behind a 24px icon. Its tint carries no
     * meaning; it exists so a grid of four cards is scannable.
     */
    .icon-tile {
        display: grid;
        place-items: center;
        inline-size: var(--space-10);
        block-size: var(--space-10);
        flex: 0 0 auto;
        border-radius: var(--radius-md);
        background-color: var(--brand-100);
        color: var(--brand-700);
    }

    .icon-tile .icon {
        inline-size: var(--space-6);
        block-size: var(--space-6);
    }

    .icon-tile--sage { background-color: var(--tint-sage); color: var(--tint-sage-ink); }
    .icon-tile--sand { background-color: var(--tint-sand); color: var(--tint-sand-ink); }
    .icon-tile--blush { background-color: var(--tint-blush); color: var(--tint-blush-ink); }
    .icon-tile--sky { background-color: var(--tint-sky); color: var(--tint-sky-ink); }
    .icon-tile--lilac { background-color: var(--tint-lilac); color: var(--tint-lilac-ink); }

    /* The design-system page's glyph list: every sprite symbol beside its name. */
    .icon-gallery {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
        gap: var(--space-3);
        margin: 0;
        padding: 0;
        list-style: none;
    }

    .icon-gallery__item {
        display: flex;
        align-items: center;
        gap: var(--space-2);
        color: var(--text);
    }

    /* ----------------------------------------------------------- stat tile */

    /*
     * docs/ux/components.md §Stat tile.
     */
    .stat-tile {
        display: flex;
        align-items: center;
        gap: var(--space-4);
        padding: var(--space-5);
        border: 1px solid var(--border);
        border-radius: var(--radius-lg);
        background-color: var(--surface-card);
        box-shadow: var(--shadow-card);
    }

    .stat-tile__body {
        display: flex;
        flex-direction: column;
        gap: var(--space-1);
        min-inline-size: 0;
    }

    .stat-tile__label {
        color: var(--text-muted);
        font-size: var(--font-size-label);
    }

    .stat-tile__figure {
        color: var(--text-strong);
        font-size: var(--font-size-metric);
        font-weight: var(--font-weight-bold);
    }

    /*
     * The period-over-period delta (issue #306) — optional, `null` by
     * default (templates/components/stat_tile.html.twig), so the dashboard's
     * own tiles render exactly as before. The triangle is aria-hidden: the
     * sign and the words in the translated text carry the meaning
     * (docs/ux/components.md: "must not be the only carriers of meaning").
     */
    .stat-tile__delta {
        font-size: var(--font-size-caption);
    }

    .stat-tile__delta--up { color: var(--status-success-fg); }
    .stat-tile__delta--down { color: var(--status-danger-fg); }
    .stat-tile__delta--flat { color: var(--text-muted); }

    /* ------------------------------------------------------- sparkline tile */

    /*
     * docs/ux/components.md §Sparkline tile. The tile has no surface of its own:
     * it sits inside the statistics card, so the stat tile gives up its border
     * and shadow too and the columns are split by dividers (dashboard.css).
     */
    .sparkline-tile {
        display: flex;
        flex-direction: column;
        gap: var(--space-2);
        min-inline-size: 0;
    }

    .sparkline-tile .stat-tile {
        padding: 0;
        border: none;
        box-shadow: none;
        background-color: transparent;
    }

    .sparkline-tile__chart {
        flex: none;
        padding-block-end: var(--space-3);
    }

    /* A tile with no chart keeps its note in the chart's band: the bottom of the tile. */
    .sparkline-tile__note {
        margin: 0;
        margin-block-start: auto;
        padding-block-end: var(--space-2);
        color: var(--text-muted);
        font-size: var(--font-size-caption);
    }

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

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

    /*
     * Cards do not nest visually more than one level deep. An inner grouping is
     * a band, not a second bordered white card.
     */
    .card__group {
        padding: var(--space-4);
        border-radius: var(--radius-md);
        background-color: var(--surface-band);
    }

    /*
     * A band that holds buttons, not prose: they sit in one wrapping row with a
     * step between them rather than touching, whatever the markup around each
     * one (a form per transition, a bare link).
     */
    .card__group--actions {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-3);
    }

    /* A record's lifecycle bar: its state at the leading edge, the one verb that changes it at the trailing edge (#821). */
    .card__group--lifecycle {
        justify-content: space-between;
    }

    /* On a phone the buttons of a confirm form are one full-width stack, so the way back is as easy to hit as the submit. */
    @media (max-width: 640px) {
        .card__group--actions > .button,
        .card__group--actions > form {
            flex: 1 1 100%;
        }

        .card__group--actions > form > .button {
            inline-size: 100%;
        }
    }

    /* ------------------------------------------------------- section header */

    .section-header {
        display: flex;
        flex-wrap: wrap;
        align-items: baseline;
        justify-content: space-between;
        gap: var(--space-3);
        margin-block-end: var(--space-4);
    }

    /*
     * The trailing link is a real link with meaningful text. Never "Ver más"
     * alone where several appear on one page — a screen reader user gets a list
     * of identical links.
     */
    .section-header__link {
        font-size: var(--font-size-label);
        font-weight: var(--font-weight-medium);
        white-space: nowrap;
    }

    /* --------------------------------------------------------- status pill */

    /*
     * The status name is the text. Colour is redundant reinforcement, never the
     * carrier — docs/ux/flows/breeder-dashboard.md.
     */
    .status-pill {
        display: inline-block;
        padding: var(--pill-padding-block) var(--pill-padding-inline);
        border-radius: var(--radius-full);
        text-align: center;
        font-size: var(--font-size-caption);
        font-weight: var(--font-weight-medium);
        line-height: var(--line-height-body);
        white-space: nowrap;
    }

    .status-pill--success {
        background-color: var(--status-success-bg);
        color: var(--status-success-fg);
    }

    .status-pill--warning {
        background-color: var(--status-warning-bg);
        color: var(--status-warning-fg);
    }

    .status-pill--info {
        background-color: var(--status-info-bg);
        color: var(--status-info-fg);
    }

    .status-pill--danger {
        background-color: var(--status-danger-bg);
        color: var(--status-danger-fg);
    }

    .status-pill--neutral {
        background-color: var(--surface-band);
        color: var(--text-muted);
    }

    /* -------------------------------------------------------------- button */

    /* `display` on .button would otherwise beat the user-agent [hidden] rule,
       and a control hidden until its Stimulus island connects would show. */
    .button[hidden] {
        display: none;
    }

    .button {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: var(--space-2);
        min-block-size: var(--touch-target);
        padding: var(--space-3) var(--space-5);
        border: 1px solid transparent;
        border-radius: var(--radius-md);
        font-family: var(--font-body);
        font-size: var(--font-size-body);
        font-weight: var(--font-weight-medium);
        text-decoration: none;
        cursor: pointer;
    }

    .button--primary {
        background-color: var(--brand-600);
        color: var(--surface-card);
    }

    .button--primary:hover {
        background-color: var(--brand-700);
        color: var(--surface-card);
    }

    .button--secondary {
        border-color: var(--brand-200);
        background-color: var(--surface-card);
        color: var(--brand-700);
    }

    .button--secondary:hover {
        background-color: var(--brand-50);
        color: var(--brand-700);
    }

    .button--ghost {
        padding-inline: var(--space-2);
        background-color: transparent;
        color: var(--brand-600);
    }

    .button--ghost:hover {
        color: var(--brand-700);
    }

    /* Full width inside its container — the sign-in submit, never a grid of these. */
    .button--block {
        inline-size: 100%;
    }

    /*
     * Disabled reduces opacity *and* sets aria-disabled — appearance alone is
     * not a state, so the selector reads the attribute rather than a class.
     */
    .button[aria-disabled='true'],
    .button:disabled {
        opacity: 0.5;
        cursor: not-allowed;
    }

    /* An icon-only button is square, and always carries an accessible name. */
    .button--icon {
        min-inline-size: var(--touch-target);
        padding: var(--space-2);
        border-radius: var(--radius-md);
        background-color: transparent;
        color: var(--text-muted);
    }

    .button--icon:hover {
        background-color: var(--brand-50);
        color: var(--brand-700);
    }

    /*
     * Destructive variants (ADR 0196). Red is --status-danger-fg and never
     * alone: the icon button carries a trash glyph and the dialog's confirm
     * button repeats the verb.
     */
    .button--danger {
        background-color: var(--status-danger-fg);
        color: var(--surface-card);
    }

    .button--danger:hover {
        background-color: var(--status-danger-fg);
        color: var(--surface-card);
        filter: brightness(0.9);
    }

    .button--icon-danger {
        min-block-size: var(--touch-target);
        color: var(--status-danger-fg);
    }

    .button--icon-danger:hover {
        background-color: var(--status-danger-bg);
        color: var(--status-danger-fg);
    }

    /* ------------------------------------------------------- confirm dialog */

    /*
     * A native <dialog> opened with showModal(): focus trap, Escape and an
     * inert page behind come from the browser. docs/ux/components.md §Confirm dialog.
     */
    .confirm-dialog {
        inline-size: min(28rem, calc(100vw - 2 * var(--space-4)));
        padding: var(--space-6);
        border: 1px solid var(--border);
        border-radius: var(--radius-lg);
        background-color: var(--surface-card);
        color: var(--text);
    }

    .confirm-dialog::backdrop {
        background-color: var(--text);
        opacity: 0.4;
    }

    .confirm-dialog__title {
        margin: 0 0 var(--space-3);
    }

    .confirm-dialog__body {
        margin: 0 0 var(--space-5);
    }

    .confirm-dialog__actions {
        display: flex;
        flex-wrap: wrap;
        justify-content: flex-end;
        gap: var(--space-3);
    }

    /* ------------------------------------------------------------ auth card */

    /*
     * A single centred card on --surface-page — the whole screen for the
     * shell-less auth flows. docs/ux/flows/breeder-sign-in.md.
     */
    .auth {
        display: flex;
        flex-direction: column;
        gap: var(--space-5);
        inline-size: 100%;
        max-inline-size: 25rem;
        margin-inline: auto;
    }

    /* A page of prose (the legal texts) takes a reading column, not a form's 400px. */
    .auth--reading {
        max-inline-size: 40rem;
    }

    .auth__brand {
        display: flex;
        align-items: center;
        justify-content: center;
        color: var(--text-strong);
    }

    /* Felio's logotype (ADR 0205): letters in currentColor, the cat-headed «o» in the brand colour. */
    .auth__mark {
        flex: none;
    }

    .auth__mark-cat {
        stroke: var(--brand-600);
    }

    .auth-card {
        display: flex;
        flex-direction: column;
        gap: var(--space-5);
        padding: var(--space-5);
        border: 1px solid var(--border);
        border-radius: var(--radius-lg);
        background-color: var(--surface-card);
        box-shadow: var(--shadow-card);
    }

    @media (width >= 26em) {
        .auth-card {
            padding: var(--space-6);
        }
    }

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

    .auth-card__subtitle {
        color: var(--text-muted);
    }

    /* A status card that leads with a heading (the unknown card): the heading centres over its centred message. */
    .auth-card__title {
        text-align: center;
    }

    /* The card already pads; an empty state's own block padding on top of it left the action 64px from its message. */
    .auth-card .empty-state {
        padding-block: var(--space-2);
    }

    /* A secondary link beneath a form: "forgot your password", "back to sign in". */
    .auth-card__footer {
        text-align: center;
    }

    /* Two or more secondary links beneath a form, one step apart. */
    .auth-card__links {
        display: flex;
        flex-direction: column;
        align-items: center;
    }

    .auth-card__links a,
    .auth-card__footer a {
        display: inline-flex;
        align-items: center;
        min-block-size: var(--touch-target);
    }

    /* The "o" between the password form and the Google link, set on a rule. */
    .auth-card__divider {
        display: flex;
        align-items: center;
        gap: var(--space-3);
        color: var(--text-muted);
    }

    .auth-card__divider::before,
    .auth-card__divider::after {
        content: '';
        flex: 1;
        border-block-start: 1px solid var(--border);
    }

    /*
     * Google's "G", in Google's colours (its sign-in branding guidelines).
     * Inline in the template rather than in the sprite, because the sprite's
     * .icon rule strokes in currentColor and this mark is four fills.
     */
    .google-mark {
        inline-size: var(--space-5);
        block-size: var(--space-5);
        flex: 0 0 auto;
    }

    .google-mark__blue { fill: var(--google-blue); }
    .google-mark__green { fill: var(--google-green); }
    .google-mark__yellow { fill: var(--google-yellow); }
    .google-mark__red { fill: var(--google-red); }

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

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

    /* Two submits side by side, primary first (docs/ux/patterns.md, "Actions"). */
    .form__actions {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-3);
    }

    /* Below 901px each button keeps the full column, primary first. */
    @media (max-width: 900px) {
        .form__actions > .button {
            flex: 1 1 100%;
        }
    }

    /* form_widget(form) wraps the rows in one div, so .form's gap does not
       reach them: the same step separates one field's hint from the next label. */
    .form__rows {
        display: flex;
        flex-direction: column;
        gap: var(--space-4);
    }

    /*
     * Control sizing, docs/ux/components.md §Control sizing. Below 901px (the
     * breeder shell's breakpoint) every control keeps the full width the column
     * gives it; from 901px a submit sizes to its label and a search form is one
     * row whose field stops at --form-max-width.
     */
    @media (min-width: 901px) {
        .form > .button {
            align-self: flex-start;
        }

        .form--search {
            flex-direction: row;
            align-items: flex-end;
        }

        /* The field itself, or the wrapper <div> form_widget puts around the
           rows of a form rendered whole (templates/form/fields.html.twig). */
        .form--search > :not(.button) {
            flex: 1 1 auto;
            max-inline-size: var(--form-max-width);
        }

        /* `.form > .button` above pins a submit to the top; a search form's
           button sits level with the bottom of its control instead (#685). */
        .form--search > .button {
            flex: none;
            align-self: flex-end;
        }

        /* Any other form: a single text control stops at --form-max-width
           instead of stretching across the column. */
        .form:not(.form--search) > :not(.button, .form-error-summary, .form-section, .form-section__actions) {
            max-inline-size: var(--form-max-width);
        }

        /* A form whose fields hold long text (a guide title, a reason, a body)
           takes the wider measure; it is opted into with .form--wide (#762). */
        .form--wide:not(.form--search) > :not(.button, .form-error-summary, .form-section, .form-section__actions) {
            max-inline-size: var(--form-wide-max-width);
        }

        /* One field and its submit: the form's 16px gap reads as the button
           belonging to the label, so the submit gets one more step (#762). */
        .form > .form__rows + .button,
        .form > .form__rows + .form__actions {
            margin-block-start: var(--space-2);
        }
    }

    /*
     * Sectioned form, docs/ux/patterns.md §Sectioned form. A fieldset per
     * group of fields, each with a legend. Below 901px the legend is
     * visually hidden but still announced, and the section adds no box, so
     * the phone keeps its one column. From 901px the sections stack with a
     * rule between them and the fields sit on a six-track grid; from 80rem
     * the legend takes a left track (3 of 12) and the fields the other nine.
     * Auto-placement only: source order is visual order is tab order.
     */
    .form-section {
        min-inline-size: 0;
        margin: 0;
        padding: 0;
        border: 0;
    }

    .form-section__legend {
        padding: 0;
        /* A floated legend is an ordinary block, not the fieldset's rendered
           legend, so it can sit beside the fields and be a grid item. */
        float: inline-start;
        position: absolute;
        inline-size: 1px;
        block-size: 1px;
        margin: -1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .form-section__fields {
        display: flex;
        flex-direction: column;
        gap: var(--space-3);
        min-inline-size: 0;
    }

    .form-section__cell {
        min-inline-size: 0;
    }

    @media (min-width: 901px) {
        .form-section {
            display: grid;
            grid-template-columns: minmax(0, 1fr);
            row-gap: var(--space-4);
            padding-block-start: var(--space-6);
            border-block-start: 1px solid var(--border);
        }

        .form-section:first-of-type {
            padding-block-start: 0;
            border-block-start: 0;
        }

        .form-section__legend {
            position: static;
            inline-size: 100%;
            block-size: auto;
            margin: 0;
            overflow: visible;
            clip-path: none;
            white-space: normal;
            color: var(--text-strong);
            font-size: var(--font-size-section-title);
            font-weight: var(--font-weight-semibold);
        }

        .form-section__fields {
            display: grid;
            grid-template-columns: repeat(6, minmax(0, 1fr));
            column-gap: var(--space-5);
            row-gap: var(--space-4);
            align-items: start;
            max-inline-size: calc(3 * var(--form-max-width) + 2 * var(--space-5));
        }

        .form-section__cell {
            grid-column: 1 / -1;
        }

        .form-section__cell--half {
            grid-column: span 3;
        }

        .form-section .field__control {
            max-inline-size: var(--form-max-width);
        }

        .form-section__actions {
            padding-block-start: var(--space-6);
            border-block-start: 1px solid var(--border);
        }
    }

    /* Three short fields on one row need room: below 64rem they stack. */
    @media (min-width: 64rem) {
        .form-section__cell--third {
            grid-column: span 2;
        }
    }

    @media (min-width: 80rem) {
        .form-section {
            grid-template-columns: minmax(0, 3fr) minmax(0, 9fr);
            column-gap: var(--grid-gutter);
        }

        .form-section__actions {
            padding-inline-start: calc((100% - var(--grid-gutter)) * 3 / 12 + var(--grid-gutter));
        }
    }

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

    .field__label {
        color: var(--text-muted);
        font-size: var(--font-size-label);
        font-weight: var(--font-weight-medium);
    }

    .field__control {
        inline-size: 100%;
        min-block-size: var(--touch-target);
        padding: var(--space-3) var(--space-4);
        border: 1px solid var(--border-control);
        border-radius: var(--radius-md);
        background-color: var(--surface-card);
        color: var(--text);
        /* At least 16px, or iOS zooms the page when the field takes focus. */
        font-size: var(--font-size-control);
    }

    /*
     * A textarea is several lines, not one: it opens three touch targets tall
     * and grows with its text up to 60vh, then scrolls, so the last line is
     * never cut off at the border with nothing to say there is more (#821).
     * Browsers without field-sizing keep the minimum and the vertical handle. A
     * textarea that sets its own `rows` (the update body, a lead note) keeps it.
     */
    textarea.field__control:not([rows]) {
        min-block-size: calc(var(--touch-target) * 3);
        max-block-size: 60vh;
        line-height: var(--line-height-body);
        field-sizing: content;
        resize: vertical;
    }

    .field__control:hover {
        border-color: var(--brand-200);
    }

    .field__control:focus-visible {
        border-color: var(--brand-600);
    }

    /*
     * A control with a button inside its right edge — the password field's
     * show/hide (issue #453). The input always reserves the button's width, so
     * revealing the button with JavaScript moves nothing; the button is out of
     * flow, a --touch-target square.
     */
    .field__control-wrap {
        position: relative;
    }

    .field__control--with-action {
        padding-inline-end: var(--touch-target);
    }

    .field__action {
        position: absolute;
        inset-block: 0;
        inset-inline-end: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        inline-size: var(--touch-target);
        padding: 0;
        border: 0;
        background: transparent;
        color: var(--text-muted);
        cursor: pointer;
    }

    /* Edge draws its own reveal button inside a password input; ours replaces it. */
    .field__control--with-action::-ms-reveal {
        display: none;
    }

    .field__action[hidden] {
        display: none;
    }

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

    .field__action-icon--hide,
    .is-revealed .field__action-icon--show {
        display: none;
    }

    .is-revealed .field__action-icon--hide {
        display: block;
    }

    /* ------------------------------------------------------- form field hint/errors */

    .field__hint {
        color: var(--text-muted);
        font-size: var(--font-size-label);
    }

    /*
     * The field's own errors, beneath its control. Red is never the message —
     * the text is — so only a thin rule and the ink colour carry any colour
     * here, matching .form-error-summary's own restraint.
     */
    .field__errors {
        display: flex;
        flex-direction: column;
        gap: var(--space-1);
        color: var(--status-danger-fg);
        font-size: var(--font-size-label);
    }

    /* An invalid control gets a visible border change, never colour alone —
       the message text beneath it is what actually says what is wrong. */
    .field__control[aria-invalid='true'] {
        border-color: var(--status-danger-fg);
    }

    /*
     * A checkbox's own row: the box and its label read as one line rather than
     * a caption stacked above an input, and the row is tall enough to be a
     * comfortable touch target on its own even though the checkbox itself is
     * small. A two-column grid, not a flex row: column one is the box, column
     * two is the label with the help and errors beneath it. (A bare
     * display:flex here once kept .field's flex-direction: column, stacking
     * and centring the box above its label.)
     */
    .field--checkbox {
        display: grid;
        grid-template-columns: auto 1fr;
        column-gap: var(--space-3);
        row-gap: var(--space-1);
        /* The box lines up with the label's first line, so a long consent text does not float it mid-height. */
        align-items: start;
        padding-block: var(--space-2);
        min-block-size: var(--touch-target);
    }

    .field--checkbox > input {
        grid-column: 1;
        grid-row: 1;
        /* The UA's default checkbox margin would indent the box from the form's left edge. */
        margin: calc(var(--space-1) / 2) 0 0;
    }

    .field--checkbox > :not(input) {
        grid-column: 2;
    }

    .field--checkbox .field__label {
        color: var(--text);
        font-weight: var(--font-weight-regular);
    }

    .field--checkbox input {
        inline-size: var(--space-5);
        block-size: var(--space-5);
    }

    /* --------------------------------------------------- form error summary */

    /*
     * Form-level only on an authentication failure: no link to a field, no
     * aria-invalid on either of them. Which half was wrong is exactly what
     * must not be disclosed, in the copy or in the markup —
     * docs/ux/flows/breeder-sign-in.md.
     *
     * The danger colour draws the rule down the side; the message itself is
     * --text-strong, which reads at 15:1 on the tint. Colour never carries the
     * meaning here — the error is the text.
     */
    .form-error-summary {
        display: flex;
        flex-direction: column;
        gap: var(--space-1);
        padding: var(--space-4);
        border-inline-start: 4px solid var(--status-danger-fg);
        border-radius: var(--radius-md);
        background-color: var(--status-danger-bg);
        color: var(--text-strong);
    }

    .form-error-summary__hint {
        color: var(--text);
    }

    /* --------------------------------------------------------- choice chips */

    /*
     * A radio group rendered as tappable chips — templates/form/fields.html.twig
     * §choice_row, docs/ux/components.md §Choice chips. <fieldset> resets its
     * UA-default border/padding/min-inline-size (the last is what would
     * otherwise let a long chip row push the group past its container's
     * width, no-horizontal-overflow's actual failure mode) and reuses .field's
     * column layout — the modern "fieldset is a block container" behaviour
     * every evergreen browser now implements makes that flex column apply
     * between the legend and .choice-chips__options like any other two flex
     * items, which is what the explicit gap below relies on rather than
     * assuming.
     */
    .choice-chips {
        border: 0;
        margin: 0;
        padding: 0;
        min-inline-size: 0;
    }

    /* Firefox pads a <legend> by default; Chromium does not — reset both to
       the same explicit spacing rather than leaving it to differ. */
    .choice-chips > legend {
        padding: 0;
        margin-block-end: var(--space-2);
    }

    .choice-chips__options {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-2);
    }

    /*
     * On a phone every option takes its own full-width row: a label such as
     * "Medio / Pregunta precios o fechas" stays on one line inside the pill, and
     * the stack is even instead of ragged. 640px is the project's phone
     * breakpoint, shared with landing.css. A multi-select of short labels
     * (`.choice-chips--inline`, the life stages) keeps the wrapping row at
     * every width: full-width bars read as text inputs, not as a choice (#821).
     */
    @media (max-width: 640px) {
        .choice-chips:not(.choice-chips--inline) .choice-chips__options {
            display: grid;
            grid-template-columns: 1fr;
        }

        .choice-chips:not(.choice-chips--inline) .choice-chips__option,
        .choice-chips:not(.choice-chips--inline) .choice-chips__option .chip {
            inline-size: 100%;
        }
    }

    /*
     * From 641px a form chip's helper sits on the label's own line, so a group
     * whose options carry one (Interés inicial) is as tall as one whose do not
     * (Canal, Raza). A phone's chips are full-width rows and keep the helper
     * beneath.
     */
    @media (min-width: 641px) {
        .choice-chips__option .chip {
            flex-direction: row;
            align-items: center;
            justify-content: center;
            column-gap: var(--space-2);
        }
    }

    /*
     * Positions the hidden input relative to its own chip rather than the
     * whole options row, and is what the focus-ring and checkmark rules below
     * anchor to — input and .chip stay adjacent siblings either way, which is
     * all the input:checked + .chip / input:focus-visible + .chip selectors
     * below actually require. The input itself carries .visually-hidden
     * (assets/styles/base.css, applied in choice_widget_expanded) rather than
     * repeating its clip-path technique here — that class is also what hides
     * the outline the global :focus-visible rule would otherwise draw around
     * the input's own (invisible) box, so the ring below is the only one a
     * keyboard user ever sees.
     */
    .choice-chips__option {
        position: relative;
        display: inline-flex;
    }

    /*
     * The chip is the input's own <label> — a real, focusable-by-proxy
     * control with no role of its own to set: the hidden radio already
     * carries role=radio, and screen readers read a <label for> as part of
     * that control's name, not as a second, separately-announced element.
     */
    .chip {
        display: inline-flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: var(--space-1);
        min-block-size: var(--touch-target);
        padding-block: var(--pill-padding-block);
        /* Both sides reserve the checkmark badge's room (--space-6), so the
           label is centred between equal padding and nothing shifts size
           when a chip becomes checked (#880). */
        padding-inline: var(--pill-padding-inline-check);
        text-align: center;
        border: 1px solid var(--border-control);
        border-radius: var(--radius-full);
        background-color: var(--surface-card);
        color: var(--text);
        font-size: var(--font-size-body);
        /* A chip is a control, not prose: base.css underlines every link. */
        text-decoration: none;
        cursor: pointer;
    }

    /*
     * Hover is drawn on the whole pill — border and fill — and is keyed on the
     * pointer being over the option wrapper as well as the chip itself: the
     * reservation form stretches a transparent radio over its chip, so the
     * pointer lands on the input and `.chip:hover` alone never fires (#879).
     * --brand-100 with a --brand-500 border is visibly distinct from the
     * resting card fill, from the solid --brand-600 selected state and from
     * the focus ring. A selected chip keeps its own, darker hover below.
     */
    .chip:hover,
    .choice-chips__option:hover > input:not(:checked) + .chip {
        border-color: var(--brand-500);
        background-color: var(--brand-100);
    }

    /* A chip that is a navigation (<a>) has no checkmark to make room for. */
    .chip--link {
        flex-direction: row;
        align-items: center;
        padding-inline: var(--pill-padding-inline);
    }

    /*
     * The selected chip is shown by fill — brand-600 under the card colour,
     * well over 4.5:1 — and by weight, so colour is never the only cue. The
     * link chip carries it as `.is-active` beside `aria-current`; the form chip
     * as `:checked` on its input.
     */
    .chip.is-active,
    .choice-chips__option input:checked + .chip {
        border-color: var(--brand-600);
        background-color: var(--brand-600);
        color: var(--surface-card);
        font-weight: var(--font-weight-semibold);
    }

    .chip.is-active:hover,
    .choice-chips__option:hover > input:checked + .chip {
        border-color: var(--brand-700);
        background-color: var(--brand-700);
    }

    .chip__helper {
        color: var(--text-muted);
        font-size: var(--font-size-label);
    }

    /*
     * The checkmark badge: present in the markup for every chip, visible only
     * on the checked one. Fixed at the chip's trailing edge, centred on its
     * height, rather than laid out inline, so it never disturbs where the label text or the helper line
     * wrap — --pill-padding-inline-check on both sides of .chip above is what reserves its
     * room, on every chip, whether checked or not, so nothing shifts size
     * when a chip becomes checked.
     */
    .icon.chip__check {
        display: none;
        position: absolute;
        inset-block-start: 50%;
        inset-inline-end: var(--space-2);
        transform: translateY(-50%);
        inline-size: var(--space-4);
        block-size: var(--space-4);
        color: var(--surface-card);
    }

    /*
     * The checked state lives on the input — :checked — never on a class a
     * controller would toggle (the fill is in the .chip rules above). Colour
     * is never the only signal: the checkmark badge appears.
     */
    .choice-chips__option input:checked + .chip .chip__check {
        display: inline-flex;
    }

    .choice-chips__option input:checked + .chip .chip__helper {
        color: var(--surface-card);
    }

    /*
     * The ring the global :focus-visible rule (assets/styles/base.css) would
     * draw on the input itself is invisible — clipped away above along with
     * the rest of its box — so it is redrawn here, on the chip that is
     * actually visible, the same width and offset as everywhere else.
     */
    .choice-chips__option input:focus-visible + .chip {
        outline: var(--focus-ring-width) solid var(--brand-600);
        outline-offset: var(--focus-ring-offset);
    }

    /* ---------------------------------------------------------- filter bar */

    /*
     * templates/components/filter_chips.html.twig: a labelled row of link
     * chips per group. The label sits beside its chips on a desktop so a group
     * is one row, and above them on a phone, where a row of chips already fills
     * the width. Groups behind "Más filtros" stack under it.
     */
    .filter-bar,
    .filter-bar__more-groups {
        display: flex;
        flex-direction: column;
        gap: var(--space-2);
    }

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

    .filter-bar__label {
        color: var(--text-muted);
        font-size: var(--font-size-label);
        font-weight: var(--font-weight-medium);
    }

    .filter-bar__options {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-2);
    }

    .filter-bar__more > summary {
        display: inline-flex;
        align-items: center;
        min-block-size: var(--touch-target);
        padding-inline: var(--space-3);
        color: var(--brand-700);
        font-size: var(--font-size-label);
        font-weight: var(--font-weight-medium);
        cursor: pointer;
    }

    .filter-bar__more[open] > summary {
        margin-block-end: var(--space-2);
    }

    @media (min-width: 901px) {
        .filter-bar__group {
            flex-direction: row;
            align-items: center;
            gap: var(--space-2);
        }

        /* Wide enough for "Entregada", the longest label of a group always on
           show (Ventas), which at the narrower column ran into its first chip
           (#685); the longer labels of the groups under "Más filtros" get a
           wider column still. */
        .filter-bar__label {
            flex: none;
            inline-size: calc(var(--space-16) + var(--space-2));
        }

        .filter-bar__more-groups .filter-bar__label {
            inline-size: calc(var(--space-16) + var(--space-8));
        }

        .filter-bar__options {
            column-gap: var(--space-1);
        }
    }

    /* ------------------------------------------------------------ list row */

    /*
     * Rows are separated by a 1px border, not by shadows or alternating fill.
     *
     * A row that opens a record (.list-row--link) is clickable end to end
     * through ONE link, .list-row__link, stretched over the row by its ::after.
     * Assistive technology still finds a single, named link; the chevron is
     * decorative. Inline links, forms and buttons are raised above the
     * stretched link, so they stay their own controls. A row with no
     * destination, or with a verb action (download, upload), is not clickable.
     */
    .list-row {
        display: flex;
        align-items: center;
        gap: var(--space-3);
        padding-block: var(--space-3);
        border-block-end: 1px solid var(--border);
    }

    .list-row:last-child {
        border-block-end: none;
    }

    /* A row wrapped in its own <li> is always its parent's last child, so
     * the rule above would strip every divider. The wrapper is what is last. */
    li > .list-row:last-child {
        border-block-end: 1px solid var(--border);
    }

    li:last-child > .list-row:last-child {
        border-block-end: none;
    }

    .list-row__body {
        flex: 1 1 auto;
        min-inline-size: 0;
    }

    .list-row__label {
        color: var(--text-strong);
        font-weight: var(--font-weight-medium);
    }

    .list-row__meta {
        color: var(--text-muted);
        font-size: var(--font-size-label);
        overflow-wrap: anywhere;
    }

    .list-row__segment {
        white-space: nowrap;
    }

    .list-row__action {
        flex: 0 0 auto;
        margin-inline-start: auto;
    }

    .list-row--link {
        position: relative;
        padding-inline: var(--space-2);
        margin-inline: calc(-1 * var(--space-2));
        border-radius: var(--radius-md);
        cursor: pointer;
    }

    /*
     * The row box is wider than its content by the hover inset, so the divider
     * is drawn inset by the same amount: it stays on the content edge, level
     * with the band above, instead of overhanging it. The real border keeps its
     * height and goes transparent.
     */
    .list-row--link:is(.list-row, .automation-row) {
        border-block-end-color: transparent;
    }

    .list-row--link::before {
        content: '';
        position: absolute;
        inset-inline: var(--space-2);
        inset-block-end: -1px;
        border-block-end: 1px solid var(--border);
    }

    .list-row--link:last-child::before {
        display: none;
    }

    /* The label keeps the row's own colour: the row is the target, not a run of link text. */
    .list-row__link,
    .list-row__link:hover {
        color: inherit;
    }

    .list-row__link::after {
        content: '';
        position: absolute;
        inset: 0;
        border-radius: var(--radius-md);
    }

    /* The ring traces the whole row, not the label it is attached to. */
    .list-row__link:focus-visible {
        outline: none;
    }

    .list-row__link:focus-visible::after {
        outline: var(--focus-ring-width) solid var(--brand-600);
        outline-offset: calc(-1 * var(--focus-ring-width));
    }

    .list-row--link a:not(.list-row__link),
    .list-row--link button,
    .list-row--link form,
    .list-row--link summary,
    .list-row--link input {
        position: relative;
        z-index: 1;
    }

    .list-row--link:hover,
    .list-row--link:has(.list-row__link:focus-visible) {
        background-color: var(--brand-50);
    }

    /* A row that is itself the one <a> (quick actions, activity, useful links)
     * gets the same box and tint, so every clickable row on a screen agrees. */
    a.list-row {
        padding-inline: var(--space-2);
        margin-inline: calc(-1 * var(--space-2));
        border-radius: var(--radius-md);
        position: relative;
        border-block-end-color: transparent;
    }

    /* Same inset divider as .list-row--link: on the content edge, not the hover box. */
    a.list-row::before {
        content: '';
        position: absolute;
        inset-inline: var(--space-2);
        inset-block-end: -1px;
        border-block-end: 1px solid var(--border);
    }

    a.list-row:last-child::before,
    li:last-child > a.list-row::before {
        display: none;
    }

    a.list-row:hover {
        background-color: var(--brand-50);
    }

    /* The box is the size the old icon button was, so the row keeps its height. */
    .list-row__chevron {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        flex: 0 0 auto;
        min-inline-size: var(--touch-target);
        min-block-size: var(--touch-target);
        color: var(--text-muted);
    }

    .list-row--link:hover .list-row__chevron,
    .list-row--link:has(.list-row__link:focus-visible) .list-row__chevron {
        color: var(--brand-700);
    }

    @media (prefers-reduced-motion: no-preference) {
        .list-row__chevron {
            transition: transform 0.15s ease;
        }

        .list-row--link:hover .list-row__chevron,
        .list-row--link:has(.list-row__link:focus-visible) .list-row__chevron {
            transform: translateX(var(--space-1));
        }
    }

    /*
     * The reorder pair: up and down share one outlined segmented box, so they read
     * as a control of their own and not as two more chevrons beside the open
     * chevron. Each half keeps the --touch-target hit area.
     */
    .reorder-group {
        display: inline-flex;
        align-items: stretch;
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        margin-inline-end: var(--space-2);
    }

    .reorder-group form + form {
        border-inline-start: 1px solid var(--border);
    }

    /* No overflow clip: it would cut the focus ring. The outer corners are rounded instead. */
    .reorder-group .button--icon {
        min-block-size: var(--touch-target);
        border-radius: 0;
    }

    .reorder-group form:first-child .button--icon {
        border-start-start-radius: calc(var(--radius-md) - 1px);
        border-end-start-radius: calc(var(--radius-md) - 1px);
    }

    .reorder-group form:last-child .button--icon {
        border-start-end-radius: calc(var(--radius-md) - 1px);
        border-end-end-radius: calc(var(--radius-md) - 1px);
    }

    /* ----------------------------------------------------- quick action row */

    /*
     * The whole row is the link (templates/components/quick_action_row.html.twig).
     * It keeps .list-row for the gap and padding, but every row is the only
     * child of its <li>, so .list-row:last-child would strip every separator:
     * the separator is drawn between the <li>s instead.
     */
    .quick-action-row {
        min-block-size: var(--touch-target);
        border-block-end: none;
    }

    li + li > .quick-action-row {
        border-block-start: 1px solid var(--border);
    }

    .quick-action-row__body {
        display: flex;
        flex: 1 1 auto;
        flex-direction: column;
        min-inline-size: 0;
    }

    .quick-action-row__title {
        color: var(--text-strong);
        font-weight: var(--font-weight-bold);
    }

    .quick-action-row__description {
        overflow: hidden;
        color: var(--text-muted);
        font-size: var(--font-size-label);
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .quick-action-row__affordance {
        display: inline-flex;
        flex: 0 0 auto;
        align-items: center;
        justify-content: center;
        color: var(--text-muted);
    }

    .quick-action-row__affordance--plus {
        inline-size: var(--space-6);
        block-size: var(--space-6);
        border-radius: 50%;
        background: var(--surface-band);
        color: var(--text-strong);
    }

    /* ---------------------------------------------------------- empty state */

    /*
     * Every list has one. On the breeder dashboard empty is often good news and
     * reads as one; on the owner portal it explains and never asks her to
     * configure anything.
     */
    .empty-state {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: var(--space-3);
        padding: var(--space-8) var(--space-4);
        text-align: center;
    }

    .empty-state__message {
        color: var(--text-muted);
        max-inline-size: 40ch;
    }

    /* A short status sentence in a card breaks evenly rather than leaving two words on the last line. */
    .auth-card .empty-state__message {
        text-wrap: balance;
    }

    /*
     * The compact empty state — docs/ux/components.md §Empty state. A band
     * with the icon beside the sentence, for a page that stacks several
     * sections that may all be empty at once (the owner's home): a tall
     * centred block per section is most of a phone screen of nothing.
     */
    .empty-state--compact {
        flex-direction: row;
        align-items: center;
        gap: var(--space-4);
        padding: var(--space-4);
        border-radius: var(--radius-md);
        background-color: var(--surface-band);
        text-align: start;
    }

    .empty-state--compact .empty-state__message {
        max-inline-size: none;
    }

    /* ---------------------------------------------------- detail page header */

    /*
     * The back link, status pill and primary actions that surround a record's
     * own <h1> — docs/ux/components.md §Detail page header. The record's own
     * heading lives in the calling page's page_title block, not here — see
     * templates/components/detail_header.html.twig.
     */
    .detail-header {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        justify-content: space-between;
        gap: var(--space-3);
        margin-block-end: var(--space-2);
    }

    .detail-header__back {
        display: inline-flex;
        align-items: center;
        gap: var(--space-1);
        color: var(--text-muted);
        font-size: var(--font-size-label);
        font-weight: var(--font-weight-medium);
    }

    .detail-header__back:hover {
        color: var(--brand-700);
    }

    .detail-header__actions {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-3);
        margin-inline-start: auto;
    }

    /* ------------------------------------------------------------ fact list */

    /*
     * Label/value pairs where "not recorded" is a real value in text, never a
     * blank cell — docs/ux/components.md §Fact list. Stacked label-over-value
     * at every width: the same simple layout the placeholder dashboard's own
     * .definitions used, kept here as the real component now that more than
     * one page needs it.
     */
    .fact-list {
        display: flex;
        flex-direction: column;
        gap: var(--space-4);
    }

    .fact-list__row + .fact-list__row {
        padding-block-start: var(--space-4);
        border-block-start: 1px solid var(--border);
    }

    .fact-list__label {
        color: var(--text-muted);
        font-size: var(--font-size-label);
        font-weight: var(--font-weight-medium);
    }

    .fact-list__value {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-3);
        margin-block-start: var(--space-1);
        /* A long unbroken token (a URL, say) must wrap, not widen the page. */
        overflow-wrap: anywhere;
        color: var(--text-strong);
        font-weight: var(--font-weight-medium);
    }

    .fact-list__value--unrecorded {
        color: var(--text-subtle);
        font-weight: var(--font-weight-regular);
    }

    /*
     * A long fact list on a wide screen — the cat's Ficha, issue #677 — shares
     * two columns so the summary and the whole first tab still fit the first
     * screen. A modifier on the list, not a change to its default: every other
     * list stays stacked at every width.
     */
    @media (min-width: 900px) {
        .fact-list--columns {
            display: grid;
            grid-template-columns: repeat(2, minmax(0, 1fr));
            gap: var(--space-4) var(--space-8);
        }

        .fact-list--columns .fact-list__row:nth-child(-n + 2) {
            padding-block-start: 0;
            border-block-start: 0;
        }
    }

    /*
     * A kitten-status group that is genuinely empty but is not an invitation
     * to do anything — RESERVED and DELIVERED before a Reservation entity
     * exists, or any group once the litter already has kittens elsewhere.
     * Deliberately plainer than .empty-state: no icon tile, no boxed padding,
     * because those visual cues are exactly what makes an empty state read as
     * a call to action, and this one is a fact.
     */
    .status-group__empty {
        color: var(--text-muted);
        font-size: var(--font-size-label);
    }

    /*
     * One kitten-status group on a record page: its heading, then its rows, as
     * a unit. The heading sits a step above its own rows and the groups sit a
     * larger step apart, so a heading reads with what is beneath it rather than
     * with the group before.
     */
    .status-group {
        display: flex;
        flex-direction: column;
        gap: var(--space-2);
    }

    .status-group > .section-header {
        margin-block-end: 0;
    }

    .status-group + .status-group {
        margin-block-start: var(--space-4);
    }

    /* -------------------------------------------------------- sidebar nav */

    .nav-item {
        display: flex;
        align-items: center;
        gap: var(--space-3);
        padding: var(--space-3);
        border-radius: var(--radius-md);
        color: var(--text);
        font-size: var(--font-size-body);
        font-weight: var(--font-weight-medium);
        text-decoration: none;
    }

    .nav-item .icon {
        color: var(--text-muted);
    }

    .nav-item:hover {
        background-color: var(--brand-50);
        color: var(--text);
    }

    /*
     * Active state is conveyed by aria-current="page", not only by colour — so
     * the selector reads the attribute. A class here would let the two drift.
     */
    .nav-item[aria-current='page'] {
        background-color: var(--brand-600);
        color: var(--surface-card);
    }

    .nav-item[aria-current='page'] .icon {
        color: var(--surface-card);
    }

    /*
     * A destination whose screen is not built yet (issue #461): muted, no
     * hover, no pointer. It has no href, so it is not focusable either; the
     * "Próximamente" tag carries the reason in words, not just in colour.
     */
    .nav-item--unavailable,
    .nav-item--unavailable:hover {
        background-color: transparent;
        color: var(--text-muted);
        cursor: default;
    }

    .nav-item__tag {
        margin-inline-start: auto;
        font-size: var(--font-size-caption);
        font-weight: var(--font-weight-regular);
    }

    .pending-link {
        display: flex;
        flex-wrap: wrap;
        align-items: baseline;
        gap: var(--space-1) var(--space-2);
        color: var(--text-muted);
        cursor: default;
    }

    /* The gap spaces the tag, so a tag that wraps under its label stays on the label's left edge. */
    .pending-link .nav-item__tag {
        margin-inline-start: 0;
    }

    .nav-group__title {
        margin-block: var(--space-5) var(--space-2);
        padding-inline: var(--space-3);
        color: var(--text-muted);
        font-size: var(--font-size-caption);
        font-weight: var(--font-weight-semibold);
        letter-spacing: 0.08em;
        text-transform: uppercase;
    }

    /* --------------------------------------------------------------- badge */

    /*
     * A decorative dot. The count itself is announced as part of the control's
     * accessible name — "Notificaciones, 2 sin leer" — so this element is
     * aria-hidden and the name comes from the catalogue.
     */
    .badge {
        position: absolute;
        inset-block-start: 0;
        inset-inline-end: 0;
        display: grid;
        place-items: center;
        min-inline-size: var(--space-5);
        block-size: var(--space-5);
        padding-inline: var(--space-1);
        border-radius: var(--radius-full);
        background-color: var(--brand-600);
        color: var(--surface-card);
        font-size: var(--font-size-caption);
        font-weight: var(--font-weight-semibold);
        line-height: 1;
    }

    .badge-anchor {
        position: relative;
        display: inline-flex;
    }

    /* -------------------------------------------------------------- avatar */

    .avatar {
        display: grid;
        place-items: center;
        inline-size: var(--space-10);
        block-size: var(--space-10);
        flex: 0 0 auto;
        border-radius: var(--radius-full);
        background-color: var(--brand-100);
        color: var(--brand-700);
        font-size: var(--font-size-label);
        font-weight: var(--font-weight-semibold);
        overflow: hidden;
    }

    .avatar img {
        inline-size: 100%;
        block-size: 100%;
        object-fit: cover;
    }

    /* ----------------------------------------------------------- data table */

    /*
     * docs/ux/components.md §Data table. Header in muted caption type, rows
     * separated by --border, no zebra and no vertical rules. Below 900px each
     * row is a card and every cell reads "label  value" from its data-label;
     * the header row is clipped, not removed, so the table stays a table to a
     * screen reader.
     */
    .data-table {
        inline-size: 100%;
        border-collapse: collapse;
    }

    .data-table th {
        padding: var(--space-2) var(--space-3);
        color: var(--text-muted);
        font-size: var(--font-size-label);
        font-weight: var(--font-weight-semibold);
        text-align: start;
        border-block-end: 1px solid var(--border);
    }

    .data-table td {
        padding: var(--space-4) var(--space-3);
        vertical-align: middle;
        border-block-end: 1px solid var(--border);
    }

    .data-table tbody tr:last-child td {
        border-block-end: none;
    }

    /*
     * A row with a primary record marks that link .data-table__row-link and the
     * whole <tr> opens it: the same one-stretched-link rule as .list-row--link.
     */
    .data-table tbody tr:has(.data-table__row-link) {
        position: relative;
        cursor: pointer;
    }

    .data-table__row-link::after {
        content: '';
        position: absolute;
        inset: 0;
        border-radius: var(--radius-md);
    }

    /* Any other control in the row stays above the stretched link. */
    .data-table tbody tr:has(.data-table__row-link) :is(a:not(.data-table__row-link), button, form, summary, input) {
        position: relative;
        z-index: 1;
    }

    .data-table__row-link:focus-visible {
        outline: none;
    }

    .data-table__row-link:focus-visible::after {
        outline: var(--focus-ring-width) solid var(--brand-600);
        outline-offset: calc(-1 * var(--focus-ring-width));
    }

    .data-table tbody tr:has(.data-table__row-link):hover,
    .data-table tbody tr:has(.data-table__row-link:focus-visible) {
        background-color: var(--brand-50);
    }

    .data-table__entity {
        display: flex;
        align-items: center;
        gap: var(--space-3);
        min-inline-size: 0;
    }

    .data-table__text {
        display: flex;
        flex-direction: column;
        min-inline-size: 0;
    }

    .data-table__primary {
        color: var(--text-strong);
        font-weight: var(--font-weight-semibold);
        overflow-wrap: anywhere;
    }

    .data-table__secondary {
        color: var(--text-muted);
        font-size: var(--font-size-label);
    }

    /* The "nothing recorded" value: muted like a secondary line, at body size. */
    .data-table__secondary--body {
        font-size: var(--font-size-body);
    }

    @media (max-width: 900px) {
        .data-table thead {
            position: absolute;
            inline-size: 1px;
            block-size: 1px;
            overflow: hidden;
            clip-path: inset(50%);
        }

        .data-table,
        .data-table tbody {
            display: block;
        }

        /*
         * One compact card per row (#685): the first cell, the record, takes
         * the full width unlabelled; every other cell sits beside the next with
         * its label above its value, so a five-line stack becomes three.
         */
        .data-table tr {
            display: flex;
            flex-wrap: wrap;
            gap: var(--space-2) var(--space-3);
            padding: var(--space-4);
            margin-block-end: var(--space-3);
            border: 1px solid var(--border);
            border-radius: var(--radius-md);
        }

        .data-table tbody tr:last-child {
            margin-block-end: 0;
        }

        .data-table td,
        .data-table tbody tr:last-child td {
            display: flex;
            flex-direction: column;
            align-items: flex-start;
            flex: 1 1 auto;
            min-inline-size: 0;
            gap: var(--space-1);
            padding: 0;
            border-block-end: none;
        }

        .data-table td:first-child,
        .data-table tbody tr:last-child td:first-child {
            flex-basis: 100%;
        }

        /* The trailing cell is a date or an amount: it keeps its line, the others give way. */
        .data-table td:last-child {
            white-space: nowrap;
        }

        .data-table td:not(:first-child) {
            font-size: var(--font-size-label);
        }

        .data-table td:first-child::before {
            display: none;
        }

        .data-table td::before {
            content: attr(data-label);
            color: var(--text-muted);
            font-size: var(--font-size-caption);
        }
    }

    /* ---------------------------------------------- content image preview */

    /*
     * The already-uploaded image on a Guide or Product edit screen —
     * docs/ux/components.md §Content image preview. Square rather than
     * circular: unlike Avatar, this is a preview of an uploaded asset, not a
     * stand-in for a person or a cat.
     */
    .content-image-preview {
        display: block;
        inline-size: var(--space-16);
        block-size: var(--space-16);
        border-radius: var(--radius-md);
        object-fit: cover;
    }

    /* ------------------------------------------------------- file upload */

    /*
     * The one upload control — templates/components/file_upload.html.twig,
     * docs/ux/components.md §File upload. The real <input type="file"> is
     * stretched transparently over the whole control row, so a click or tap
     * anywhere on it opens the picker and a keyboard or screen reader meets
     * the input itself; the button-like span below it is drawing only. The
     * transparent input draws no outline of its own, so the focus ring is
     * carried by the sibling button, the same technique as the choice chips.
     */
    .file-upload {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        gap: var(--space-2);
    }

    .file-upload__control {
        position: relative;
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-3);
        max-inline-size: 100%;
    }

    .file-upload__input {
        position: absolute;
        inset: 0;
        inline-size: 100%;
        block-size: 100%;
        margin: 0;
        padding: 0;
        opacity: 0;
        cursor: pointer;
    }

    /* A refused file changes the control, not only the message under it —
       the same rule as .field__control[aria-invalid] above. */
    .file-upload__input[aria-invalid='true'] + .file-upload__button {
        border-color: var(--status-danger-fg);
    }

    .file-upload__input:hover + .file-upload__button {
        background-color: var(--brand-50);
    }

    .file-upload__input:focus-visible + .file-upload__button {
        outline: var(--focus-ring-width) solid var(--brand-600);
        outline-offset: var(--focus-ring-offset);
    }

    .file-upload__name {
        min-inline-size: 0;
        color: var(--text-muted);
        font-size: var(--font-size-label);
        overflow-wrap: anywhere;
    }

    .file-upload__limits {
        margin: 0;
    }

    /* ----------------------------------------------------------- callout */

    /*
     * A tinted panel with an icon and a supporting line — docs/ux/components.md
     * §Callout. Used as the app's flash region (templates/shell/breeder.html.twig),
     * one tone per flash type. The three tones reuse the same --status-* tokens
     * the status pill already does, rather than inventing a fourth colour
     * scale for the same three meanings.
     */
    .callout {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-3);
        padding: var(--space-4);
        border-radius: var(--radius-md);
        background-color: var(--surface-band);
    }

    /* A basis wide enough that a trailing action wraps under the message on a
       phone instead of squeezing it into a six-line column (#685). */
    .callout__message {
        flex: 1 1 14rem;
        margin: 0;
        font-size: var(--font-size-body);
        color: var(--text);
    }

    /*
     * The one trailing link a callout may carry (issue #302) — the lead
     * quick-entry form's own "Guardar y añadir otro" callout pointing at the
     * contact just saved. Ghost-styled, matching .button--ghost's own
     * treatment for an inline action, rather than a second filled button
     * competing with the message.
     */
    .callout__action {
        margin-inline-start: auto;
        color: var(--brand-600);
        font-size: var(--font-size-label);
        font-weight: var(--font-weight-medium);
        white-space: nowrap;
    }

    .callout__action:hover {
        color: var(--brand-700);
    }

    .callout--success {
        background-color: var(--status-success-bg);
    }

    .callout--success .callout__message {
        color: var(--status-success-fg);
    }

    .callout--warning {
        background-color: var(--status-warning-bg);
    }

    .callout--warning .callout__message {
        color: var(--status-warning-fg);
    }

    .callout--error {
        background-color: var(--status-danger-bg);
    }

    .callout--error .callout__message {
        color: var(--status-danger-fg);
    }

    /* ---------------------------------------------------------- action bar */

    /*
     * docs/ux/components.md §Action bar. Not needed on desktop — the control
     * it repeats is already reachable there without scrolling past a long
     * one-column form — so it renders nothing above the breeder shell's own
     * 900px breakpoint (assets/styles/shell-breeder.css).
     *
     * `position: fixed`, not `sticky`: the control must be reachable
     * *without scrolling at all* (docs/ux/flows/lead-quick-entry.md's own
     * acceptance criterion), on first paint, whatever the bar's own place in
     * source order — and a `sticky` element only pins once scrolling has
     * already carried it to its offset, which never happens for a bar that
     * lives near the end of a long form precisely so the normal focus order
     * reaches it last (docs/ux/components.md's own "never traps focus,
     * stays in the normal focus order").
     */
    .action-bar {
        display: none;
    }

    @media (max-width: 900px) {
        .action-bar {
            display: block;
            position: fixed;
            inset-inline: 0;
            inset-block-end: 0;
            z-index: 1;
            padding: var(--space-3) var(--space-4);
            background-color: var(--surface-page);
            border-block-start: 1px solid var(--border);
        }
    }

    /* -------------------------------------------------------------- band */

    /*
     * The persistent cards pinned to the bottom of both sidebars — the plan
     * card and the help card on the breeder shell, the trust card and the help
     * card on the owner shell. A tinted panel, never a bordered white card.
     */
    .panel {
        display: flex;
        flex-direction: column;
        gap: var(--space-2);
        padding: var(--space-4);
        border-radius: var(--radius-md);
        background-color: var(--surface-band);
        font-size: var(--font-size-label);
    }

    .panel__title {
        display: flex;
        align-items: center;
        gap: var(--space-2);
        color: var(--text-strong);
        font-weight: var(--font-weight-semibold);
    }

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

    /* ---------------------------------------------------------- line chart */

    /*
     * docs/ux/components.md §Line chart. The SVG scales with its box and its
     * viewBox fixes the aspect ratio, so there is no pixel width anywhere.
     * Every colour is a token, applied by class: ADR 0028's CSP leaves no room
     * for a `style` attribute or a presentation colour.
     */
    .line-chart {
        display: block;
        inline-size: 100%;
        block-size: auto;
        overflow: visible;
        font-family: var(--font-body);
    }

    .line-chart__grid {
        stroke: var(--border);
        stroke-width: 1;
    }

    .line-chart__tick,
    .line-chart__label {
        fill: var(--text-muted);
        font-size: var(--font-size-caption);
    }

    .line-chart__area {
        fill: var(--brand-100);
        stroke: none;
        opacity: 0.7;
    }

    .line-chart__line {
        fill: none;
        stroke: var(--brand-600);
        stroke-width: 2;
        stroke-linejoin: round;
        stroke-linecap: round;
    }

    .line-chart__dot {
        fill: var(--brand-600);
        stroke: var(--surface-card);
        stroke-width: 1;
    }

    /* The Resumen de ventas panel on the dashboard. */
    .dashboard-sales__chart {
        display: flex;
        flex-direction: column;
        gap: var(--space-2);
        margin: 0;
    }

    .dashboard-sales__total {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: var(--space-1);
        margin: 0;
    }

    .dashboard-sales__total-link {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: var(--space-1);
        text-decoration: none;
    }

    /* Drawn as the stat tile figure; the caption alone carries the link mark. */
    .dashboard-sales__total-figure {
        color: var(--text-strong);
        font-size: var(--font-size-metric);
        font-weight: var(--font-weight-bold);
        text-decoration: none;
    }

    .dashboard-sales__total-label {
        color: var(--brand-600);
        font-size: var(--font-size-caption);
        text-decoration: underline;
    }

    .dashboard-sales__legend {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: var(--space-2);
        color: var(--text-muted);
        font-size: var(--font-size-caption);
    }

    .dashboard-sales__swatch {
        inline-size: 8px;
        block-size: 8px;
        border-radius: var(--radius-full);
        background-color: var(--brand-600);
    }

    .dashboard-sales__note {
        margin: 0;
        color: var(--text-muted);
        font-size: var(--font-size-label);
        text-align: center;
    }
}

/*
 * Temporary. templates/staff/dashboard.html.twig is a placeholder that proves
 * who is signed in and where the door out is — the real breeder dashboard
 * (stat tiles, the reservation pipeline) is issue #68, and its own comment
 * says to replace the template wholesale rather than extend it. This block
 * goes with it; delete both together. It is layout, not a reusable
 * component, so it sits in the shell layer even though shell-breeder.css
 * never loads on this page — the placeholder extends base.html.twig
 * directly, not the real shell, so its styles have to live somewhere every
 * page already links, and components.css is that file.
 */
@layer shell {
    .page {
        max-inline-size: 60rem;
        margin-inline: auto;
        padding: var(--space-8) var(--space-4);
    }

    .page__header {
        margin-block-end: var(--space-6);
    }

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

    .definitions dd + dt {
        margin-block-start: var(--space-4);
    }

    .definitions dt {
        color: var(--text-muted);
        font-size: var(--font-size-label);
        font-weight: var(--font-weight-medium);
    }

    .definitions dd {
        color: var(--text-strong);
        font-weight: var(--font-weight-medium);
    }
}

/*
 * Tabs — docs/ux/components.md §Tabs. The server renders a list of links and
 * every panel; tabs_controller.js adds the roles and hides the unselected
 * panels, so these rules style the enhanced state through the ARIA
 * attributes it sets and leave the unenhanced one a plain list of links.
 */
@layer components {
    .tabs {
        display: flex;
        flex-direction: column;
        gap: var(--space-6);
        min-inline-size: 0;
    }

    .tabs__list {
        position: relative;
        display: flex;
        flex-wrap: nowrap;
        gap: var(--space-1);
        margin: 0;
        padding: 0;
        list-style: none;
        overflow-x: auto;
        box-shadow: inset 0 -1px 0 var(--border);
        scrollbar-width: thin;
    }

    /*
     * A scroll cue for a row wider than its box (a phone showing the last tab
     * clipped): the trailing edge fades into the page, above the tab text, for
     * as long as there is more to scroll to. tabs_controller.js sets
     * data-overflow on the nav and clears it at the end of the row.
     */
    .tabs__nav {
        position: relative;
    }

    .tabs__nav::after {
        content: '';
        position: absolute;
        inset-block: 0;
        inset-inline-end: 0;
        inline-size: var(--space-10);
        background-image: linear-gradient(to left, var(--surface-page), transparent);
        opacity: 0;
        pointer-events: none;
    }

    .tabs__nav[data-overflow]::after {
        opacity: 1;
    }

    .tabs__list > li {
        flex: none;
    }

    .tabs__tab {
        display: inline-flex;
        align-items: center;
        min-block-size: var(--touch-target);
        padding: 0 var(--space-4);
        border-block-end: 3px solid transparent;
        color: var(--text-muted);
        font-weight: var(--font-weight-medium);
        text-decoration: none;
        white-space: nowrap;
    }

    .tabs__tab:hover {
        color: var(--text-strong);
    }

    .tabs__tab[aria-selected='true'] {
        border-block-end-color: var(--brand-600);
        color: var(--brand-700);
    }

    .tabs__tab:focus-visible {
        outline: var(--focus-ring-width) solid var(--brand-600);
        outline-offset: calc(var(--focus-ring-width) * -1);
        border-radius: var(--radius-sm);
    }

    .tabs__panel {
        display: flex;
        flex-direction: column;
        gap: var(--space-6);
        min-inline-size: 0;
    }

    .tabs__panel[hidden] {
        display: none;
    }
}

/*
 * Detail summary — issue #677. The few facts that identify a record, in one
 * row that wraps, above the tabs of a detail page (cat, person, lead). The
 * reservation detail's own `.reservation-summary` predates it. Tokens only.
 */
@layer components {
    .detail-summary {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        justify-content: space-between;
        gap: var(--space-4) var(--space-8);
        padding: var(--space-4) var(--space-6);
    }

    .detail-summary__facts {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-4) var(--space-8);
        margin: 0;
    }

    .detail-summary__facts dt {
        color: var(--text-muted);
        font-size: var(--font-size-label);
        font-weight: var(--font-weight-medium);
    }

    .detail-summary__facts dd {
        margin: 0;
        color: var(--text-strong);
        font-weight: var(--font-weight-semibold);
    }

    /* A value not recorded reads as an absence, like the fact list's own. */
    .detail-summary__facts dd.detail-summary__value--unrecorded {
        color: var(--text-subtle);
        font-weight: var(--font-weight-regular);
    }

    /* On a phone the facts share two column lines instead of wrapping inline. */
    @media (max-width: 600px) {
        .detail-summary__facts {
            display: grid;
            grid-template-columns: repeat(2, minmax(0, 1fr));
            inline-size: 100%;
            gap: var(--space-4);
        }

        .detail-summary__facts dd {
            overflow-wrap: anywhere;
        }

        /* A long value — an email address — takes the whole row, not half of it. */
        .detail-summary__fact--wide {
            grid-column: 1 / -1;
        }
    }

    /* ------------------------------------------------- data chart family */

    /*
     * docs/ux/components.md §Bar chart, §Ring, §Funnel, §Stacked bar, §Legend
     * and docs/ux/design-system.md §Data series. Marks are filled with the
     * category tints' -ink shades (the pale tints fail 3:1 on a white card);
     * series n is always the same hue, in the order lilac, sky, sage, sand,
     * blush, and 0 — no series — is the brand violet. The one exception is the stacked bar and its legend, whose touching segments use their own order, below. Hue never carries
     * meaning alone: every mark has a printed label or a legend row.
     */
    .bar-chart__bar,
    .bar-chart__bar--0 { fill: var(--brand-500); }
    .bar-chart__bar--1 { fill: var(--tint-lilac-ink); }
    .bar-chart__bar--2 { fill: var(--tint-sky-ink); }
    .bar-chart__bar--3 { fill: var(--tint-sage-ink); }
    .bar-chart__bar--4 { fill: var(--tint-sand-ink); }
    .bar-chart__bar--5 { fill: var(--tint-blush-ink); }

    /* A partial month: outlined and pale, so a short bar is not read as a dip. */
    .bar-chart__bar--partial {
        fill: var(--brand-100);
        stroke: var(--brand-500);
        stroke-width: 1.5;
    }

    .bar-chart--vertical {
        display: block;
        inline-size: 100%;
        block-size: auto;
        overflow: visible;
        font-family: var(--font-body);
    }

    .bar-chart__rows,
    .funnel__stages {
        display: flex;
        flex-direction: column;
        gap: var(--space-3);
        margin: 0;
        padding: 0;
        list-style: none;
    }

    .bar-chart__row {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas: "name value" "track track";
        align-items: baseline;
        column-gap: var(--space-3);
        row-gap: var(--space-1);
    }

    .bar-chart__name { grid-area: name; color: var(--text-strong); font-size: var(--font-size-label); overflow-wrap: anywhere; }
    .bar-chart__value { grid-area: value; color: var(--text-strong); font-size: var(--font-size-label); font-weight: var(--font-weight-bold); font-variant-numeric: tabular-nums; }
    .bar-chart__track { grid-area: track; }

    .bar-chart__track,
    .funnel__track,
    .stacked-bar__svg {
        display: block;
        inline-size: 100%;
        overflow: hidden;
        border-radius: var(--radius-full);
    }

    .bar-chart__track { block-size: 8px; }
    .funnel__track { block-size: 14px; }
    .stacked-bar__svg { block-size: 16px; }

    .bar-chart__rail,
    .funnel__rail { fill: var(--surface-band); }

    /* The stage tiles' own hues: Recibidos sky, Reserva creada sage, Vendidos sand. */
    .funnel__bar--1 { fill: var(--tint-sky-ink); }
    .funnel__bar--2 { fill: var(--tint-sage-ink); }
    .funnel__bar--3 { fill: var(--tint-sand-ink); }

    .funnel__stage {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas: "label value" "track track";
        align-items: baseline;
        column-gap: var(--space-3);
        row-gap: var(--space-1);
    }

    .funnel__label { grid-area: label; color: var(--text-strong); font-size: var(--font-size-label); }
    .funnel__value { grid-area: value; color: var(--text-strong); font-size: var(--font-size-label); font-weight: var(--font-weight-bold); font-variant-numeric: tabular-nums; }
    .funnel__track { grid-area: track; }

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

    /* Touching segments are read as neighbours, so their order is the one the validator passes at the normal-vision floor: lilac, sand, sky, sage, blush (design-system.md §Data series). */
    .stacked-bar__segment--1 { fill: var(--tint-lilac-ink); }
    .stacked-bar__segment--2 { fill: var(--tint-sand-ink); }
    .stacked-bar__segment--3 { fill: var(--tint-sky-ink); }
    .stacked-bar__segment--4 { fill: var(--tint-sage-ink); }
    .stacked-bar__segment--5 { fill: var(--tint-blush-ink); }

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

    .legend__item {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr) auto;
        align-items: baseline;
        gap: var(--space-2);
        font-size: var(--font-size-label);
    }

    .legend__swatch {
        inline-size: 10px;
        block-size: 10px;
        border-radius: var(--radius-sm);
        background: var(--brand-500);
    }

    .legend__swatch--1 { background: var(--tint-lilac-ink); }
    .legend__swatch--2 { background: var(--tint-sand-ink); }
    .legend__swatch--3 { background: var(--tint-sky-ink); }
    .legend__swatch--4 { background: var(--tint-sage-ink); }
    .legend__swatch--5 { background: var(--tint-blush-ink); }

    .legend__label { color: var(--text-strong); }
    .legend__value { color: var(--text-muted); font-variant-numeric: tabular-nums; }

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

    .ring__svg {
        flex: none;
        inline-size: 88px;
        block-size: 88px;
    }

    .ring__track,
    .ring__arc {
        fill: none;
        stroke-width: 12;
    }

    .ring__track { stroke: var(--surface-band); }
    .ring__arc { stroke: var(--brand-500); stroke-linecap: round; }
    .ring__arc--1 { stroke: var(--tint-lilac-ink); }
    .ring__arc--2 { stroke: var(--tint-sky-ink); }
    .ring__arc--3 { stroke: var(--tint-sage-ink); }
    .ring__arc--4 { stroke: var(--tint-sand-ink); }
    .ring__arc--5 { stroke: var(--tint-blush-ink); }

    .ring__value {
        fill: var(--text-strong);
        font-family: var(--font-body);
        font-size: var(--font-size-card-title);
        font-weight: var(--font-weight-bold);
    }

    .ring__text {
        display: flex;
        flex-direction: column;
        gap: var(--space-1);
        min-inline-size: 0;
    }

    .ring__label { color: var(--text-strong); font-size: var(--font-size-label); font-weight: var(--font-weight-medium); }
    .ring__caption { color: var(--text-muted); font-size: var(--font-size-caption); }

    /* The data table behind a chart, folded under it: closed it is one line. */
    .chart-data {
        border-block-start: 1px solid var(--border);
        padding-block-start: var(--space-3);
    }

    .chart-data > summary {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: var(--space-4);
        min-block-size: var(--touch-target);
        color: var(--text-muted);
        font-size: var(--font-size-label);
        cursor: pointer;
        list-style: none;
    }

    .chart-data > summary::-webkit-details-marker {
        display: none;
    }

    /* The chevron the FAQ rows end on, turned when the data opens. */
    .chart-data > summary::after {
        content: '';
        flex: 0 0 auto;
        inline-size: var(--space-2);
        block-size: var(--space-2);
        border-inline-end: 2px solid var(--text-muted);
        border-block-end: 2px solid var(--text-muted);
        transform: rotate(45deg);
    }

    .chart-data[open] > summary::after {
        transform: rotate(-135deg);
    }

    .chart-data[open] > summary { margin-block-end: var(--space-3); }
}

/* --------------------------------------------------------------- pagination */

/*
 * docs/ux/components.md §Pagination. The count line sits above the two links;
 * the links take the full width on a phone, where a 44px target is the point.
 */
@layer components {
    .pagination {
        display: flex;
        flex-direction: column;
        gap: var(--space-3);
        margin-block-start: var(--space-4);
    }

    .pagination__summary {
        margin: 0;
        color: var(--text-muted);
        font-size: var(--font-size-label);
    }

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

    @media (min-width: 640px) {
        .pagination__nav {
            flex-direction: row;
        }
    }
}
