/* Components. Every screen of every phase is built from these; the guide is
   admin_app/views/README.md, and a new pattern belongs here, documented
   there, before a second page needs it.

   Status colours (success, warning, danger, info) appear only in badges,
   alerts, flash messages, field errors, destructive actions (the danger
   button, a window's Delete and the Sign out rows of the settings) and the
   green dot of something live (.live-dot). Everything else is black, white
   and gray: focus rings, checked controls and selected options too.
   The one other colour is a platform's mark, a soft tint of its brand
   ("Platform marks"). The violet of the sign in panel never appears on a
   control. */

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

.btn {
    --btn-fg: var(--color-text);
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: var(--control-height);
    padding: 0 var(--space-4);
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--btn-fg);
    font-size: var(--text-base);
    font-weight: var(--weight-medium);
    line-height: 1.25;
    text-align: center;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    user-select: none;
    transition: background-color var(--duration-fast) var(--ease-in-out),
                border-color var(--duration-fast) var(--ease-in-out),
                color var(--duration-fast) var(--ease-in-out),
                box-shadow var(--duration-fast) var(--ease-in-out);
}

.btn .icon {
    width: 1.125rem;
    height: 1.125rem;
}

.btn-primary {
    --btn-fg: var(--color-on-primary);
    background: var(--color-primary);
    box-shadow: var(--shadow-xs);
}

.btn-primary:active {
    background: var(--color-primary-active);
}

.btn-secondary {
    border-color: var(--color-border-strong);
    background: var(--color-surface);
    box-shadow: var(--shadow-xs);
}

.btn-secondary:active {
    background: var(--color-surface-muted);
}

.btn-ghost {
    --btn-fg: var(--color-text-muted);
}

.btn-danger {
    --btn-fg: var(--color-on-danger);
    background: var(--color-danger-solid);
    box-shadow: var(--shadow-xs);
}

/* The second action of a window that destroys, Delete beside Save changes,
   a secondary button with its word in the danger's colour (D108, point 5).
   In the window's foot it stands left of the action, and on a phone the two
   share the width (.dialog-foot); its own form asks first (data-confirm). */
.btn-danger-text {
    --btn-fg: var(--color-danger-text);
}

@media (hover: hover) {
    .btn-primary:hover {
        background: var(--color-primary-hover);
    }

    .btn-secondary:hover {
        background: var(--color-surface-muted);
    }

    .btn-ghost:hover {
        --btn-fg: var(--color-text);
        background: var(--color-surface-muted);
    }

    .btn-danger:hover {
        background: var(--color-danger-solid-hover);
    }
}

.btn-sm {
    min-height: var(--control-height-sm);
    padding: 0 var(--space-3);
    font-size: var(--text-sm);
}

.btn-block {
    display: flex;
    width: 100%;
}

/* Icon only. Needs an accessible name: aria-label, or a .sr-only span. */
.btn-icon {
    width: var(--control-height);
    padding: 0;
}

.btn-icon.btn-sm {
    width: var(--control-height-sm);
}

.btn:disabled,
.btn[aria-disabled="true"] {
    opacity: 0.5;
    box-shadow: none;
    cursor: not-allowed;
}

/* A link cannot be disabled, so aria-disabled links stop taking clicks. */
a.btn[aria-disabled="true"] {
    pointer-events: none;
}

/* Set by app.js on the button that submitted its form. The label stays in
   the layout, transparent, so the button keeps its width. */
.btn.is-loading,
.btn.is-loading:disabled {
    color: transparent;
    opacity: 1;
    cursor: progress;
}

.btn.is-loading::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 16px;
    height: 16px;
    margin: -8px 0 0 -8px;
    border: 2px solid var(--btn-fg);
    border-right-color: transparent;
    border-radius: var(--radius-full);
    animation: spin 0.6s linear infinite;
}

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

/* ------------------------------------------------------------------------
   Forms
   ------------------------------------------------------------------------ */

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

.form-row {
    display: grid;
    gap: var(--space-5);
}

/* Aligned at the bottom, so the two controls stay level when only one of
   them has an error or a help line above it. */
@media (min-width: 640px) {
    .form-row {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        align-items: end;
    }
}

.form-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    padding-top: var(--space-1);
}

/* What the action will do, first in its .form-actions: beside the buttons
   on a page, on the left of a window's foot (D101). */
.form-actions-note {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

@media (max-width: 639px) {
    .form-actions {
        flex-direction: column;
        align-items: stretch;
    }
}

/* The card of a dialog page, about the width of its window. The same page
   opens as a window over the page it was asked from (app.js), and is a
   page of its own without script or when loaded by its address. [D73] */
.form-card {
    max-width: 36rem;
}

/* A heading over fields that belong together inside a form, as
   MyPeptideApp's "Peptide", "Dose" and "Schedule": small capitals, a line
   above all but the first. */
.form-group-title {
    color: var(--color-text-subtle);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    line-height: var(--leading-tight);
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.form-group-title:not(:first-child) {
    margin-top: var(--space-2);
    padding-top: var(--space-5);
    border-top: 1px solid var(--color-border);
}

.field {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}

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

.field-optional {
    margin-left: var(--space-1);
    color: var(--color-text-subtle);
    font-weight: var(--weight-normal);
}

.field-help {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    line-height: 1.5;
}

.field-error {
    display: flex;
    align-items: flex-start;
    gap: 6px;
    color: var(--color-danger-text);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    line-height: 1.45;
}

.field-error .icon {
    width: 1rem;
    height: 1rem;
    margin-top: 1px;
}

.input,
.select,
.textarea {
    display: block;
    width: 100%;
    min-height: var(--control-height);
    padding: 0 var(--space-3);
    border: 1px solid var(--color-control-border);
    border-radius: var(--radius-sm);
    background-color: var(--color-input);
    color: var(--color-text);
    font-size: var(--text-md);
    line-height: 1.5;
    box-shadow: var(--shadow-xs);
    -webkit-appearance: none;
    appearance: none;
    transition: border-color var(--duration-fast) var(--ease-in-out),
                box-shadow var(--duration-fast) var(--ease-in-out);
}

.textarea {
    min-height: 6.5rem;
    padding: 10px var(--space-3);
    resize: vertical;
}

/* A dropdown looks like a secondary button: its border, surface and shadow,
   and its grey under the pointer. It is found by its text and chevron, as a
   button is by its label, so beside a button, such as a filter row's dates
   item, the two read as one kind of control. A text field in a form
   and a checkbox keep control-border, which an empty field needs to be
   found at all (tokens.css); a text or date field among filters takes the
   dropdown's border instead (.input-filter, below). */
.select {
    padding-right: 40px;
    border-color: var(--color-border-strong);
    background-color: var(--color-surface);
    background-image: var(--image-select-chevron);
    background-position: right 12px center;
    background-repeat: no-repeat;
    background-size: 18px;
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-in-out),
                border-color var(--duration-fast) var(--ease-in-out),
                box-shadow var(--duration-fast) var(--ease-in-out);
}

/* A text or date field among filters: the dropdown's border, surface and
   shadow, so a row of filters reads as one family. It is found by its
   label, its icon or its placeholder, as the dropdown beside it by its
   text: the area's search under a title, a filter row's dates without
   script. A field in a form keeps control-border.
   Under the pointer its border darkens a step, a field's own sign that it
   takes input; focus, an error, read only and disabled stay those of
   every field. */
.input-filter {
    border-color: var(--color-border-strong);
    background-color: var(--color-surface);
}

.input::placeholder,
.textarea::placeholder {
    color: var(--color-text-subtle);
    opacity: 1;
}

/* An empty date or time field shows the browser's format hint (such as
   dd.mm.yyyy) in place of a value. CSS cannot tell it from a filled field,
   so app.js keeps data-empty in step with the value, and the hint takes the
   placeholder's colour. */
input[data-empty]::-webkit-datetime-edit {
    color: var(--color-text-subtle);
}

/* Read only and disabled fields are grey and lose the strong border: there
   is nothing to type into, so their outline need not stand out. */
.input[readonly],
.textarea[readonly] {
    border-color: var(--color-border-strong);
    background-color: var(--color-surface-muted);
    color: var(--color-text-muted);
    box-shadow: none;
}

.input:disabled,
.select:disabled,
.textarea:disabled {
    border-color: var(--color-border-strong);
    background-color: var(--color-surface-muted);
    color: var(--color-text-subtle);
    box-shadow: none;
    cursor: not-allowed;
}

/* A darker border under the pointer says the field takes input. Not on a
   field that is focused, invalid, read only or disabled: each of those has a
   border of its own. A dropdown turns grey instead, as a secondary button
   does, and a field among filters darkens from the dropdown's border by a
   step of its own, after the fields' rule so it wins. */
@media (hover: hover) {
    .input:hover:not(:focus-visible, :disabled, [readonly], [aria-invalid="true"]),
    .textarea:hover:not(:focus-visible, :disabled, [readonly], [aria-invalid="true"]) {
        border-color: var(--color-control-border-hover);
    }

    .select:hover:not(:disabled) {
        background-color: var(--color-surface-muted);
    }

    .input-filter:hover:not(:focus-visible, :disabled, [readonly], [aria-invalid="true"]) {
        border-color: var(--color-border-strong-hover);
    }
}

/* Focus: the border in the full ink and a soft halo around it. A text field
   matches :focus-visible whenever it has the focus; a select, and a read only
   field, only when the keyboard brought it there. After the read only rules,
   so a read only field shows it too. */
.input:focus-visible,
.select:focus-visible,
.textarea:focus-visible {
    outline: none;
    border-color: var(--color-focus);
    box-shadow: 0 0 0 3px var(--color-focus-halo);
}

/* An invalid field stays red, focused or not. */
.input[aria-invalid="true"],
.select[aria-invalid="true"],
.textarea[aria-invalid="true"] {
    border-color: var(--color-danger-text);
}

.input[aria-invalid="true"]:focus-visible,
.select[aria-invalid="true"]:focus-visible,
.textarea[aria-invalid="true"]:focus-visible {
    box-shadow: 0 0 0 3px var(--color-danger-border);
}

/* Chrome paints autofilled fields in its own blue. A long inset shadow is the
   only way back, because the background colour is not honoured there, and
   the focus ring has to be repeated in the same declaration or autofilled
   fields lose it. */
.input:-webkit-autofill,
.input:-webkit-autofill:hover {
    -webkit-text-fill-color: var(--color-text);
    box-shadow: 0 0 0 1000px var(--color-input) inset;
    caret-color: var(--color-text);
}

.input:-webkit-autofill:focus {
    -webkit-text-fill-color: var(--color-text);
    box-shadow: 0 0 0 3px var(--color-focus-halo), 0 0 0 1000px var(--color-input) inset;
}

.input-with-icon {
    position: relative;
}

.input-with-icon > .icon {
    position: absolute;
    top: 50%;
    left: 12px;
    width: 18px;
    height: 18px;
    color: var(--color-text-subtle);
    transform: translateY(-50%);
    pointer-events: none;
}

.input-with-icon > .input {
    padding-left: 40px;
}

/* Password with a show and hide button. The button is hidden until app.js
   runs, because without script it could do nothing. */
.password-field {
    position: relative;
}

.password-field .input {
    padding-right: 52px;
}

.password-toggle {
    position: absolute;
    top: 0;
    right: 0;
    display: grid;
    place-items: center;
    width: var(--control-height);
    height: var(--control-height);
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--color-text-subtle);
}

.password-toggle:focus-visible {
    outline-offset: -6px;
}

@media (hover: hover) {
    .password-toggle:hover {
        color: var(--color-text);
    }
}

.password-toggle .password-hide,
.password-toggle[aria-pressed="true"] .password-show {
    display: none;
}

.password-toggle[aria-pressed="true"] .password-hide {
    display: block;
}

/* Checkboxes and radios, drawn by hand so they match the inputs: the same
   border, and when checked the colours of the primary button, a white tick
   or dot on black in light mode and the reverse in dark mode.

   The label holds only the option's name; its help is outside the label and
   reaches screen readers through aria-describedby, so the name stays short.
   The label's ::after covers the whole row, which keeps the entire row, or
   the entire card, the tap target. */
.check {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
}

.check-input {
    display: grid;
    place-items: center;
    flex: none;
    width: 20px;
    height: 20px;
    margin: 1px 0 0;
    border: 1px solid var(--color-control-border);
    /* Scaled down from --radius-sm for a 20 pixel box. */
    border-radius: calc(var(--radius-sm) - 3px);
    background-color: var(--color-input);
    background-position: center;
    background-repeat: no-repeat;
    background-size: 14px;
    -webkit-appearance: none;
    appearance: none;
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-in-out),
                border-color var(--duration-fast) var(--ease-in-out),
                box-shadow var(--duration-fast) var(--ease-in-out);
}

.check-input[type="radio"] {
    border-radius: var(--radius-full);
}

.check-input:checked {
    border-color: var(--color-primary);
    background-color: var(--color-primary);
}

.check-input[type="checkbox"]:checked {
    background-image: var(--image-check);
}

/* The dot of a radio is its on-primary background showing through a thick
   inner ring of primary. */
.check-input[type="radio"]:checked {
    background-color: var(--color-on-primary);
    box-shadow: inset 0 0 0 5px var(--color-primary);
}

/* The whole row is the target, so the box answers the pointer anywhere on
   it. */
@media (hover: hover) {
    .check:hover .check-input:not(:checked, :disabled, [aria-invalid="true"]) {
        border-color: var(--color-control-border-hover);
    }
}

.check-input:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.check-input[aria-invalid="true"] {
    border-color: var(--color-danger-text);
}

.check-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    padding-top: 1px;
}

.check-label {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-weight: var(--weight-medium);
    cursor: pointer;
}

.check-label::after {
    content: '';
    position: absolute;
    inset: 0;
}

.check-label .icon {
    width: 1rem;
    height: 1rem;
    color: var(--color-text-muted);
}

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

.check:has(.check-input:disabled) {
    color: var(--color-text-subtle);
}

.check:has(.check-input:disabled) .check-label {
    cursor: not-allowed;
}

@media (pointer: coarse) {
    .check {
        min-height: 44px;
        align-items: center;
    }

    .check-input {
        margin-top: 0;
    }
}

/* A yes or no in a window is a switch on its row (D108, point 4): its name
   and a plain line on the left, the switch on the right, grey when off and
   in ink when on, its knob the surface. The `checkbox` partial with
   'switch' => true draws it. As with a box, the label holds the name alone,
   the line reaches screen readers through aria-describedby, and the
   label's ::after covers the row, so the whole row is the target. The
   keyboard rings the switch; a click leaves none (base.css). */
.switch-row {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-4);
}

.switch-row-text {
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.switch-row-label {
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    cursor: pointer;
}

.switch-row-label::after {
    content: '';
    position: absolute;
    inset: 0;
}

.switch-row-help {
    color: var(--color-text-subtle);
    font-size: var(--text-sm);
}

.switch {
    position: relative;
    flex: none;
    width: 44px;
    height: 26px;
    margin: 0;
    border-radius: var(--radius-full);
    background: var(--color-border-strong);
    -webkit-appearance: none;
    appearance: none;
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-in-out);
}

.switch::before {
    content: '';
    position: absolute;
    top: 3px;
    left: 3px;
    width: 20px;
    height: 20px;
    border-radius: var(--radius-full);
    background: var(--color-surface);
    box-shadow: var(--shadow-xs);
    transition: transform var(--duration-fast) var(--ease-in-out);
}

.switch:checked {
    background: var(--color-text);
}

.switch:checked::before {
    transform: translateX(18px);
}

.switch:focus-visible {
    outline-offset: 2px;
}

.switch[aria-invalid="true"] {
    box-shadow: 0 0 0 1px var(--color-danger-text);
}

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

@media (pointer: coarse) {
    .switch-row {
        min-height: 44px;
    }
}

/* A group of radios or checkboxes under its legend. */
.choices > legend {
    margin-bottom: var(--space-2);
    padding: 0;
}

.choices > .field-help,
.choices > .field-error {
    margin-bottom: var(--space-2);
}

.choice-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding-top: var(--space-1);
}

/* Each option a bordered card, for choices with a description, such as the
   role of an invite. */
.choice-list-cards {
    gap: var(--space-2);
}

.choice-list-cards .check {
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    transition: border-color var(--duration-fast) var(--ease-in-out),
                background-color var(--duration-fast) var(--ease-in-out);
}

@media (hover: hover) {
    .choice-list-cards .check:hover:not(:has(.check-input:checked, .check-input:disabled)) {
        border-color: var(--color-control-border);
    }
}

.choice-list-cards .check:has(.check-input:checked) {
    border-color: var(--color-primary);
    background: var(--color-surface-muted);
}

/* The keyboard focus rings the whole card, which is the target, rather than
   the radio inside it. */
.choice-list-cards .check:has(.check-input:focus-visible) {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}

.choice-list-cards .check:has(.check-input:focus-visible) .check-input {
    outline: none;
}

/* Tiles, the statuses of a record (D107's mock): cards two by two from 560
   pixels, slimmer, the radio hidden (the tile is the target and its border
   says which is chosen), the dot of the status's tone before its name, the
   help small and grey. */
.choice-tiles {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 6px;
}

@media (min-width: 560px) {
    .choice-tiles {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

.choice-tiles .check {
    align-items: flex-start;
    gap: 10px;
    min-height: 0;
    padding: 10px 12px;
    border-color: var(--color-border);
}

.choice-tiles .check-input {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    opacity: 0;
}

.choice-tiles .check:has(.check-input:checked) {
    border-color: var(--color-text);
    background: var(--color-surface);
    box-shadow: inset 0 0 0 1px var(--color-text);
}

.choice-tiles .check-text {
    padding-top: 0;
}

.choice-tiles .check-label {
    font-size: var(--text-sm);
}

.choice-tiles .check-help {
    color: var(--color-text-subtle);
    font-size: var(--text-xs);
}

.choice-dot {
    flex: none;
    width: 8px;
    height: 8px;
    margin-top: 6px;
    border-radius: var(--radius-full);
    background: var(--color-success-solid);
}

.choice-dot.is-warning { background: var(--color-warning-text); }
.choice-dot.is-danger { background: var(--color-danger-solid); }
.choice-dot.is-neutral { background: var(--color-text-subtle); }

.choice-dot.is-outline {
    background: transparent;
    box-shadow: inset 0 0 0 1.5px var(--color-text-subtle);
}

/* A segmented control, for two to four short options such as the theme.
   Its radios are placed in it, never in a window: placed against the
   window, a radio far down a long one lies below its box, and the focus a
   segment's click gives it scrolls the whole window, head and all, out of
   sight. */
.segmented {
    position: relative;
    display: inline-flex;
    flex-wrap: wrap;
    gap: 2px;
    padding: 3px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface-muted);
}

.segmented-input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
}

.segment {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 36px;
    padding: 0 var(--space-3);
    /* Concentric with the container: its radius minus its padding. */
    border-radius: calc(var(--radius-sm) - 3px);
    color: var(--color-text-muted);
    font-weight: var(--weight-medium);
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-in-out),
                color var(--duration-fast) var(--ease-in-out);
}

.segment .icon {
    width: 1rem;
    height: 1rem;
}

@media (hover: hover) {
    .segmented-input:not(:checked, :disabled) + .segment:hover {
        color: var(--color-text);
    }
}

.segmented-input:checked + .segment {
    background: var(--color-surface-raised);
    color: var(--color-text);
    box-shadow: var(--shadow-sm);
}

.segmented-input:checked + .segment .icon {
    color: var(--color-text);
}

.segmented-input:focus-visible + .segment {
    outline: 2px solid var(--color-focus);
    outline-offset: 1px;
}

.segmented-input:disabled + .segment {
    opacity: 0.5;
    cursor: not-allowed;
}

@media (pointer: coarse) {
    .segment {
        min-height: 44px;
    }
}

/* Forced colours (Windows contrast themes) drop backgrounds and shadows,
   which is all that marks a checked box, a switch or the chosen segment
   here. The boxes and switches fall back to the system's own controls, and
   the chosen segment gets an outline in the system's text colour. */
@media (forced-colors: active) {
    .check-input,
    .switch {
        -webkit-appearance: auto;
        appearance: auto;
    }

    .switch::before {
        content: none;
    }

    .segmented-input:checked + .segment {
        outline: 2px solid;
        outline-offset: -2px;
    }
}

/* Shown under a form that is disabled while viewing as someone. */
.readonly-notice {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-top: var(--space-4);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    background: var(--color-surface-muted);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

.readonly-notice .icon {
    width: 1rem;
    height: 1rem;
}

/* The summary at the top of a form that failed validation. Focused on load
   by app.js, so it keeps a visible ring: that is where the reader now is. */
.error-summary {
    margin-bottom: var(--space-6);
    padding: var(--space-4);
    border: 1px solid var(--color-danger-border);
    border-radius: var(--radius-sm);
    background: var(--color-danger-bg);
    color: var(--color-danger-text);
}

/* Inside a form, as on the invite and reset pages, the form's own gap
   spaces it; the margin would double it. */
.form > .error-summary {
    margin-bottom: 0;
}

.error-summary:focus {
    outline: 2px solid var(--color-danger-text);
    outline-offset: 2px;
}

.error-summary-title {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    color: inherit;
    font-family: var(--font-sans);
    font-size: var(--text-md);
    letter-spacing: 0;
}

.error-summary-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    margin-top: var(--space-2);
    padding-left: 28px;
}

.error-summary-list a {
    color: inherit;
    font-weight: var(--weight-medium);
    text-decoration-color: currentColor;
}

/* A read only value with a copy button, for links shown once. */
.copy-field {
    display: flex;
    gap: var(--space-2);
}

.copy-field .input {
    font-family: var(--font-mono);
    font-size: var(--text-md);
    text-overflow: ellipsis;
}

.copy-field .btn {
    flex: none;
}

.copy-status {
    color: var(--color-success-text);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
}

.copy-status:empty {
    display: none;
}

.copy-status.is-error {
    color: var(--color-danger-text);
}

@media (max-width: 479px) {
    .copy-field {
        flex-direction: column;
    }
}

/* ------------------------------------------------------------------------
   The photo frame (partials/photo-frame.php): the field of a picture, a
   model's or a person's, the chosen photo with its frame, and the tile
   look of a model's window Edit (D95).
   ------------------------------------------------------------------------ */

/* The photo the field holds now, and the chosen one with its frame, which
   app.js shows in its place. */
.photo-frame-current,
.photo-frame-stage {
    margin: var(--space-1) 0 var(--space-2);
    border-radius: var(--radius-md);
    background: var(--color-surface-muted);
}

.photo-frame-current {
    display: block;
    width: 100%;
    max-width: 20rem;
    height: auto;
}

/* The stage hugs the whole photo, so the frame is placed in percentages
   of it, and cuts off the frame's shadow at the photo's edge. A click
   beside the frame brings it there. */
.photo-frame-stage {
    position: relative;
    width: fit-content;
    max-width: 100%;
    overflow: hidden;
    cursor: pointer;
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
}

/* Tall enough to judge the frame, short enough on a phone to leave the
   field's button and the page in reach. */
.photo-frame-image {
    display: block;
    width: auto;
    max-width: 100%;
    height: auto;
    max-height: min(28rem, 60vh);
    pointer-events: none;
}

/* The frame: its shadow dims the photo outside it. A finger moving along
   the frame's way moves the frame, and one moving across it the page. */
.photo-frame-window {
    position: absolute;
    border: 2px solid var(--color-surface);
    box-shadow: 0 0 0 100vmax var(--color-overlay);
    cursor: grab;
    touch-action: none;
}

.photo-frame-window[data-axis="x"] {
    touch-action: pan-y;
}

.photo-frame-window[data-axis="y"] {
    touch-action: pan-x;
}

.photo-frame-window.is-dragging {
    cursor: grabbing;
}

/* Inside the frame: the stage cuts off whatever lies outside the photo. */
.photo-frame-window:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: -5px;
}

/* The tile look (is-tile), a model's window Edit (D95): the picture, or a
   tile, beside its line, and the label as a secondary button. The file
   field stays out of sight, the button showing its keyboard focus (D80). */
.photo-tile-row {
    display: flex;
    align-items: center;
    gap: var(--space-4);
}

.photo-tile {
    position: relative;
    display: grid;
    place-items: center;
    flex: none;
    width: 105px;
    aspect-ratio: 21 / 19;
    overflow: hidden;
    border: 1px dashed var(--color-border-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface-muted);
    color: var(--color-text-subtle);
}

.photo-tile.has-photo {
    border-style: solid;
    border-color: var(--color-border);
}

.photo-tile img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.photo-tile > .icon {
    width: 22px;
    height: 22px;
}

.photo-tile-side {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-3);
    min-width: 0;
}

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

.photo-tile-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

.photo-tile-pick {
    cursor: pointer;
}

.photo-frame.is-tile {
    position: relative;
}

.photo-frame.is-tile .photo-frame-stage {
    margin-top: var(--space-4);
}

.photo-frame.is-tile .photo-frame-input {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.photo-frame.is-tile:has(.photo-frame-input:focus-visible) .photo-tile-pick {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}

.photo-frame.is-tile:has(.photo-frame-input:disabled) .photo-tile-pick {
    color: var(--color-text-subtle);
    box-shadow: none;
    cursor: not-allowed;
}

/* The browser's own file control, its button drawn as a secondary one. */
.photo-frame-input {
    max-width: 100%;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

.photo-frame-input::file-selector-button {
    min-height: var(--control-height-sm);
    margin-right: var(--space-3);
    padding: 0 var(--space-3);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    box-shadow: var(--shadow-xs);
    color: var(--color-text);
    font: inherit;
    font-weight: var(--weight-medium);
    cursor: pointer;
}

@media (hover: hover) {
    .photo-frame-input:not(:disabled)::file-selector-button:hover {
        background: var(--color-surface-muted);
    }
}

.photo-frame-input[aria-invalid="true"]::file-selector-button {
    border-color: var(--color-danger-text);
}

.photo-frame-input:disabled::file-selector-button {
    color: var(--color-text-subtle);
    box-shadow: none;
    cursor: not-allowed;
}

/* ------------------------------------------------------------------------
   Cards and lists
   ------------------------------------------------------------------------ */

.card {
    overflow: hidden;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    box-shadow: var(--shadow-xs);
}

.card-header {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-3) var(--space-4);
    padding: var(--space-5) var(--space-5) 0;
}

.card-header:last-child {
    padding-bottom: var(--space-5);
}

/* The title and description take the room there is and wrap inside it,
   so the card's actions stay top right while 20rem are left for the text,
   rather than wrapping under a long description. */
.card-header > :first-child {
    flex: 1 1 20rem;
    min-width: 0;
}

.card-title {
    font-size: var(--text-lg);
    letter-spacing: -0.015em;
}

/* A quiet count beside a card's title (D101): "136", "11 of 136", in the
   text's face on the title's baseline, no badge. The tabs keep their
   .tab-count. */
.card-count {
    margin-left: var(--space-1);
    color: var(--color-text-subtle);
    font-family: var(--font-sans);
    font-size: var(--text-xs);
    font-weight: var(--weight-normal);
    font-variant-numeric: tabular-nums;
    letter-spacing: 0;
}

.card-description {
    margin-top: var(--space-1);
    color: var(--color-text-muted);
}

.card-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.card-body {
    padding: var(--space-5);
}

.card-header + .card-body {
    padding-top: var(--space-4);
}

/* For a table or a list that runs to the card's edges. */
.card-header + .card-flush {
    margin-top: var(--space-4);
    border-top: 1px solid var(--color-border);
}

.card-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-5);
    border-top: 1px solid var(--color-border);
    background: var(--color-canvas);
}

@media (min-width: 640px) {
    .card-header {
        padding: var(--space-6) var(--space-6) 0;
    }

    .card-header:last-child {
        padding-bottom: var(--space-6);
    }

    .card-body {
        padding: var(--space-6);
    }

    .card-footer {
        padding: var(--space-3) var(--space-6);
    }
}

/* Rows that lead somewhere, such as a list of settings. */
.list-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-height: 60px;
    padding: var(--space-3) var(--space-5);
    border-top: 1px solid var(--color-border);
    color: inherit;
    text-decoration: none;
}

.list > li:first-child > .list-item {
    border-top: 0;
}

/* Under the pointer the row turns the grey of a hovered navigation row:
   the same in light mode as the canvas, lighter than the card in dark mode,
   where the canvas would read as a hole. */
@media (hover: hover) {
    a.list-item:hover {
        background: var(--color-surface-hover);
    }
}

/* Inside the row: the card clips everything outside its edges. */
a.list-item:focus-visible {
    outline-offset: -2px;
}

.list-icon {
    display: grid;
    place-items: center;
    flex: none;
    width: 36px;
    height: 36px;
    border-radius: var(--radius-sm);
    background: var(--color-surface-muted);
    color: var(--color-text-muted);
}

.list-icon .icon {
    width: 1.125rem;
    height: 1.125rem;
}

.list-icon-success { background: var(--color-success-bg); color: var(--color-success-text); }
.list-icon-warning { background: var(--color-warning-bg); color: var(--color-warning-text); }
.list-icon-danger { background: var(--color-danger-bg); color: var(--color-danger-text); }
.list-icon-info { background: var(--color-info-bg); color: var(--color-info-text); }

/* A handle or an address has no break of its own, and would run under the
   badge beside it on a phone. */
.list-body {
    display: flex;
    flex: 1;
    flex-direction: column;
    min-width: 0;
    overflow-wrap: anywhere;
}

.list-title {
    font-weight: var(--weight-medium);
}

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

/* The row's badges: side by side, and one under the other on a phone,
   where the row's text needs the room. */
.list-meta {
    display: flex;
    flex: none;
    flex-direction: column;
    align-items: flex-end;
    gap: var(--space-1);
    color: var(--color-text-subtle);
    font-size: var(--text-sm);
}

@media (min-width: 640px) {
    .list-meta {
        flex-direction: row;
        align-items: center;
        gap: var(--space-2);
    }
}

.list-item > .icon:last-child {
    color: var(--color-text-subtle);
}

/* Key and value pairs: a profile, a setting, the details of a job run. */
.kv-row {
    display: grid;
    gap: 2px var(--space-4);
    padding: var(--space-3) 0;
    border-top: 1px solid var(--color-border);
}

.kv-row:first-child {
    padding-top: 0;
    border-top: 0;
}

.kv-row:last-child {
    padding-bottom: 0;
}

.kv-key {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
}

.kv-value {
    min-width: 0;
    overflow-wrap: anywhere;
}

.kv-empty {
    color: var(--color-text-subtle);
}

@media (min-width: 640px) {
    .kv-row {
        grid-template-columns: minmax(8rem, 30%) minmax(0, 1fr);
        align-items: baseline;
    }
}

/* An expandable list of details, for example the rest of an audit entry. */
.disclosure > summary {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    min-height: 32px;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    list-style: none;
    cursor: pointer;
}

@media (pointer: coarse) {
    .disclosure > summary {
        min-height: 44px;
    }
}

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

.disclosure > summary .icon {
    width: 1rem;
    height: 1rem;
    transition: transform var(--duration-fast) var(--ease-in-out);
}

.disclosure[open] > summary .icon {
    transform: rotate(180deg);
}

.disclosure-body {
    margin-top: var(--space-2);
}

/* ------------------------------------------------------------------------
   Tables, in the look of Home's Models card (D83): a head in plain words
   without a fill, rows of 60 pixels, the name with its picture or mark and
   a quiet line under it (inventory.css, .inv-record), figures in tabular
   digits. A table sits in a card, a list's under a card-header with its
   title and no line under it.

   The same table on every screen: where its columns need more room than
   the card has, it scrolls sideways inside its .table-wrap, and its first
   column, the name of the row, stays in place; while more columns follow
   on the right, the wrap's right edge fades (D101). Cells keep to one
   line, so a column is as wide as its widest value; a cell marked
   .cell-wrap wraps its words instead, and so does the name on a phone.
   data-label, which named a cell on the cards a phone once showed, is no
   longer shown.
   ------------------------------------------------------------------------ */

/* The wrap's width is the card's, never its table's: without the
   containment a table wider than a phone would widen the page. Positioned,
   since a size container is no containing block: the .sr-only "Actions" of
   the last column, placed absolutely, would otherwise escape the wrap and
   scroll the whole page sideways on a phone (found 10 October 2026). It
   slides sideways only: one scrolling sideways scrolls up and down as
   well, so a table a few pixels too tall caught the wheel that scrolls the
   page (the developer's find of 21 October 2026, D101). */
.table-wrap {
    position: relative;
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    container-type: inline-size;
}

/* More columns to the right: app.js marks the wrap while its table goes
   on past its right edge, on load, as it is resized and as it slides, and
   its last 48 pixels fade until the end is reached (D101). A mask rather
   than a gradient laid over the table, so it fades into whatever lies
   under it, a card or the record panel, in either theme, and every click
   still reaches the table. */
.table-wrap.has-more-right {
    -webkit-mask-image: linear-gradient(to left, transparent 0, black 48px);
    mask-image: linear-gradient(to left, transparent 0, black 48px);
}

/* Separate borders: a sticky cell keeps its own in every browser. */
.table {
    border-collapse: separate;
    border-spacing: 0;
}

.table th {
    padding: 8px var(--space-4);
    border-bottom: 1px solid var(--color-border);
    color: var(--color-text-subtle);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    text-align: left;
    white-space: nowrap;
}

.table td {
    height: 60px;
    padding: 10px var(--space-4);
    border-top: 1px solid var(--color-border);
    vertical-align: middle;
    white-space: nowrap;
}

/* A date broken over two lines reads as two values. */
.table time {
    white-space: nowrap;
}

.table tbody tr:first-child td {
    border-top: 0;
}

.table th:first-child,
.table td:first-child {
    position: sticky;
    left: 0;
    z-index: 1;
    min-width: 11rem;
    padding-left: var(--space-5);
    background: var(--color-surface);
}

/* On a phone the name may take two lines rather than half the screen. */
@media (max-width: 639px) {
    .table td:first-child {
        max-width: 15rem;
        white-space: normal;
    }
}

.table th:last-child,
.table td:last-child {
    padding-right: var(--space-5);
}

@media (min-width: 640px) {
    .table th:first-child,
    .table td:first-child {
        padding-left: var(--space-6);
    }

    .table th:last-child,
    .table td:last-child {
        padding-right: var(--space-6);
    }
}

/* The row under the pointer, the cell that stays in place with it. */
@media (hover: hover) {
    .table tbody tr:hover td {
        background: var(--color-surface-hover);
    }
}

.cell-primary {
    font-weight: var(--weight-medium);
}

.cell-primary a {
    text-decoration: none;
}

@media (hover: hover) {
    .cell-primary a:hover {
        text-decoration: underline;
    }
}

.cell-sub {
    display: block;
    color: var(--color-text-subtle);
    font-size: var(--text-xs);
    font-weight: var(--weight-normal);
}

/* Figures line up in tabular digits: numbers, times and durations. Not the
   whole table, because Inter gives a hyphen a digit's width in tabular
   figures, which spaces out an email such as pending-admin@. */
.table .cell-num,
.table .cell-digits,
.table time {
    font-variant-numeric: tabular-nums;
}

.table .cell-num {
    text-align: right;
}

.table td.cell-num {
    font-weight: var(--weight-medium);
}

/* A cell of running words, such as a list of names: it wraps. */
.table .cell-wrap {
    min-width: 9rem;
    white-space: normal;
}

.table .cell-nowrap {
    white-space: nowrap;
}

.table .cell-actions {
    text-align: right;
}

/* A long text in a list, such as the result of a job run, cut to one line
   with an ellipsis; the whole text is on the row's own page. The column
   takes whatever room the others leave, and some even on a phone. */
.table td.cell-truncate {
    width: 100%;
    min-width: 8rem;
    max-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The buttons of a row stay on one line. */
.row-actions {
    display: inline-flex;
    justify-content: flex-end;
    gap: var(--space-2);
}

.row-actions form {
    display: flex;
}

/* ------------------------------------------------------------------------
   Status: badges, alerts, flash messages
   ------------------------------------------------------------------------ */

.badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 1px 10px 1px 8px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-full);
    background: var(--color-surface-muted);
    color: var(--color-text-muted);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    line-height: 20px;
    white-space: nowrap;
}

.badge::before {
    content: '';
    width: 6px;
    height: 6px;
    border-radius: var(--radius-full);
    background: currentColor;
}

.badge-success { border-color: var(--color-success-border); background: var(--color-success-bg); color: var(--color-success-text); }
.badge-warning { border-color: var(--color-warning-border); background: var(--color-warning-bg); color: var(--color-warning-text); }
.badge-danger { border-color: var(--color-danger-border); background: var(--color-danger-bg); color: var(--color-danger-text); }
.badge-info { border-color: var(--color-info-border); background: var(--color-info-bg); color: var(--color-info-text); }

/* Warming up, an account's status that is neither good nor bad yet: the
   outline of a badge on nothing. [Phase 4, sections 4.1 and 8.18] */
.badge-outline { border-color: var(--color-border-strong); background: transparent; color: var(--color-text-muted); }

/* The small badge of Models & Accounts, beside a node of a model's page, a
   line of a phone card, a search result and a marker beside a value. */
.badge-sm {
    gap: 5px;
    padding: 0 8px 0 6px;
    font-size: 0.6875rem;
    line-height: 18px;
}

.badge-sm::before {
    width: 5px;
    height: 5px;
}

/* A neutral label without a status, such as a role name or a permission. */
.tag {
    display: inline-flex;
    align-items: center;
    padding: 1px var(--space-2);
    border: 1px solid var(--color-border);
    border-radius: calc(var(--radius-sm) - 2px);
    background: var(--color-surface-muted);
    color: var(--color-text);
    font-size: var(--text-sm);
    line-height: 20px;
    white-space: nowrap;
}

.alert {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border: 1px solid;
    border-radius: var(--radius-sm);
    font-size: var(--text-base);
    line-height: 1.5;
}

.alert > .icon {
    margin-top: 1px;
}

.alert-body {
    flex: 1;
    min-width: 0;
}

.alert-title {
    font-weight: var(--weight-semibold);
}

.alert-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-top: var(--space-3);
}

.alert-success { border-color: var(--color-success-border); background: var(--color-success-bg); color: var(--color-success-text); }
.alert-warning { border-color: var(--color-warning-border); background: var(--color-warning-bg); color: var(--color-warning-text); }
.alert-danger { border-color: var(--color-danger-border); background: var(--color-danger-bg); color: var(--color-danger-text); }
.alert-info { border-color: var(--color-info-border); background: var(--color-info-bg); color: var(--color-info-text); }

/* A link in the text takes the alert's colour; a button among its actions
   keeps its own. */
.alert a:not(.btn) {
    color: inherit;
    font-weight: var(--weight-medium);
    text-decoration-color: currentColor;
}

/* Flash messages are alerts that confirm what just happened, shown once at
   the top of the next page, with a dismiss button when script runs. */
.flash-stack {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-bottom: var(--space-6);
}

.flash {
    animation: flash-in var(--duration-normal) var(--ease-out);
}

.flash-dismiss {
    display: grid;
    place-items: center;
    flex: none;
    width: 32px;
    height: 32px;
    margin: -6px -8px -6px 0;
    padding: 0;
    border: 0;
    border-radius: calc(var(--radius-sm) - 2px);
    background: none;
    color: inherit;
}

.flash-dismiss .icon {
    width: 1rem;
    height: 1rem;
}

@media (hover: hover) {
    .flash-dismiss:hover {
        background: color-mix(in srgb, currentColor 12%, transparent);
    }
}

/* Undo in a flash message (Phase 5, M5.7): a form of its own beside the
   words, its button underlined text in the alert's colour. */
.flash-undo {
    flex: none;
}

.flash-undo-button {
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    font-weight: var(--weight-semibold);
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
}

.flash-undo-button:disabled {
    cursor: not-allowed;
    opacity: 0.6;
}

@media (pointer: coarse) {
    .flash-dismiss {
        width: 44px;
        height: 44px;
        margin: -12px -14px -12px 0;
    }
}

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

/* The green dot of something live, the one green of a screen that is not a
   status: the live card while it refreshes itself (D116) and a person seen
   in the last 5 minutes, on their page's Devices cell and window (D118). Its
   place beside its words is the place's own. */
.live-dot {
    display: inline-block;
    flex: none;
    width: 6px;
    height: 6px;
    border-radius: var(--radius-full);
    background: var(--color-success-solid);
}

/* ------------------------------------------------------------------------
   Platform marks: an account's platform in its brand's colours
   ------------------------------------------------------------------------ */

/* The class platform-<key> on a mark (inventory/mark, the Accounts tab of
   Analytics, a result in the search) gives it its brand's tint, border and
   glyph colour from tokens.css, as --mark-bg, --mark-border and --mark-fg.
   Each mark reads them with its own neutral look as the fallback
   (.inv-mark, .an-account-icon, .search-mark), so a platform without a
   rule here, a black brand or one without a mark, keeps the grey box and
   the ink glyph. A ghost, and an ended account in a model's fold, stay
   grey. */
.platform-instagram {
    --mark-fg: var(--color-platform-instagram);
    --mark-bg: var(--color-platform-instagram-bg);
    --mark-border: var(--color-platform-instagram-border);
}

.platform-onlyfans {
    --mark-fg: var(--color-platform-onlyfans);
    --mark-bg: var(--color-platform-onlyfans-bg);
    --mark-border: var(--color-platform-onlyfans-border);
}

.platform-facebook {
    --mark-fg: var(--color-platform-facebook);
    --mark-bg: var(--color-platform-facebook-bg);
    --mark-border: var(--color-platform-facebook-border);
}

.platform-reddit {
    --mark-fg: var(--color-platform-reddit);
    --mark-bg: var(--color-platform-reddit-bg);
    --mark-border: var(--color-platform-reddit-border);
}

.platform-youtube {
    --mark-fg: var(--color-platform-youtube);
    --mark-bg: var(--color-platform-youtube-bg);
    --mark-border: var(--color-platform-youtube-border);
}

.platform-telegram {
    --mark-fg: var(--color-platform-telegram);
    --mark-bg: var(--color-platform-telegram-bg);
    --mark-border: var(--color-platform-telegram-border);
}

.platform-snapchat {
    --mark-fg: var(--color-platform-snapchat);
    --mark-bg: var(--color-platform-snapchat-bg);
    --mark-border: var(--color-platform-snapchat-border);
}

/* ------------------------------------------------------------------------
   Empty states and ghosts, pagination
   ------------------------------------------------------------------------ */

.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: var(--space-12) var(--space-6);
    text-align: center;
}

.empty-state-icon {
    display: grid;
    place-items: center;
    width: 48px;
    height: 48px;
    margin-bottom: var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface-muted);
    color: var(--color-text-muted);
}

.empty-state-title {
    font-size: var(--text-md);
    font-weight: var(--weight-semibold);
}

.empty-state-text {
    max-width: 44ch;
    margin-top: var(--space-1);
    color: var(--color-text-muted);
}

.empty-state .btn {
    margin-top: var(--space-5);
}

/* Ghosts: what a card that has nothing to show yet shows instead. The
   shape of what will come, in the ghost greys and fading out, and over it
   what is missing. On a page's main card .ghost-title, .ghost-text and the
   one action in .ghost-actions; on the others one line in a .ghost-chip.
   Nothing moves. The ghost itself is hidden from screen readers; the words
   carry it. .empty-state stays for what is not a "not yet": no matches,
   nothing found. [D77]

     .ghost-bar, .ghost-bar-sm, .ghost-dot
                         the unit of every ghost: a bar for a figure or a
                         name, a smaller one under it, a dot for a picture
     .ghost-title, .ghost-text, .ghost-actions
                         the words over a page's main ghost, in the body
                         font and the subtle grey, and its one button
     .ghost-chart        a chart's ghost: .ghost-chart-plot the line and its
                         fading area (Ghost::chart()), the title on a
                         dashed line, the block centred in the card
     .ghost-list         a list's ghost (the ghost-list partial): three rows,
                         .ghost-list-rows, a .ghost-dot (.is-square for a
                         thing) and .ghost-lines each, fading downwards, and
                         under them .ghost-list-body with the words
     .ghost-lines        a name and its line under it, as two bars
     .ghost-wrap-covers, .ghost-covers, .ghost-cover, .ghost-cover-frame
                         reel covers in a row (9 by 16, the plays' pill
                         in the corner, the handle under it), fading to the
                         right, as Home's top reels and an account's reels
                         stand, the chip on the covers' middle
     .ghost-wrap, .ghost-chip
                         a ghost's frame and the one line centred on it
     .ghost-fade-down, .ghost-fade-right
                         a ghost that fades out downwards or to the right */

.ghost-bar {
    display: block;
    height: 10px;
    border-radius: var(--radius-full);
    background: var(--color-ghost-strong);
}

.ghost-bar-sm {
    height: 8px;
    background: var(--color-ghost);
}

.ghost-dot {
    display: block;
    flex: none;
    width: 36px;
    height: 36px;
    border-radius: var(--radius-full);
    background: var(--color-ghost-strong);
}

.ghost-dot.is-square {
    border-radius: var(--radius-sm);
}

.ghost-lines {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    gap: 7px;
    min-width: 0;
}

/* Names of different lengths, so the rows read as rows. */
.ghost-lines > .ghost-bar {
    width: 46%;
}

.ghost-lines > .ghost-bar-sm {
    width: 28%;
}

li:nth-child(3n + 2) > .ghost-lines > .ghost-bar {
    width: 36%;
}

li:nth-child(3n + 2) > .ghost-lines > .ghost-bar-sm {
    width: 22%;
}

li:nth-child(3n) > .ghost-lines > .ghost-bar {
    width: 54%;
}

li:nth-child(3n) > .ghost-lines > .ghost-bar-sm {
    width: 32%;
}

.ghost-title {
    color: var(--color-text-subtle);
    font-family: var(--font-sans);
    font-size: var(--text-md);
    font-weight: var(--weight-semibold);
    letter-spacing: normal;
    line-height: var(--leading-tight);
    text-wrap: balance;
}

.ghost-text {
    max-width: 38ch;
    margin-top: 6px;
    color: var(--color-text-subtle);
    text-wrap: balance;
}

.ghost-actions {
    margin-top: var(--space-6);
}

.ghost-actions .btn {
    min-height: 42px;
    padding-inline: var(--space-5);
    border-radius: var(--radius-md);
}

/* A chart's ghost: the block centred in the card, the line under it. */
.ghost-chart {
    position: relative;
    isolation: isolate;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 320px;
    padding: var(--space-8) var(--space-5);
    overflow: hidden;
    text-align: center;
}

/* Under a card's title, the block centred in the room that is left. */
.card-header + .ghost-chart {
    min-height: 300px;
}

.ghost-chart-plot {
    position: absolute;
    right: var(--space-5);
    bottom: var(--space-5);
    left: var(--space-5);
    z-index: -1;
    height: 46%;
}

.ghost-chart-plot svg {
    display: block;
    width: 100%;
    height: 100%;
    overflow: visible;
}

/* The title on a dashed line across the card, dashes of 6 pixels. */
.ghost-chart .ghost-title {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    width: 100%;
}

.ghost-chart .ghost-title::before,
.ghost-chart .ghost-title::after {
    content: "";
    flex: 1 1 0;
    min-width: var(--space-4);
    height: 1px;
    background: linear-gradient(90deg, var(--color-border) 0 6px, transparent 6px) 0 0 / 12px 1px repeat-x;
}

/* Both start their dashes at the title. */
.ghost-chart .ghost-title::before {
    background-position: right 0 top 0;
}

.ghost-chart .ghost-title > span {
    flex: 0 1 auto;
    max-width: 26ch;
}

@media (min-width: 640px) {
    .ghost-chart {
        padding-inline: var(--space-6);
    }

    .ghost-chart-plot {
        right: var(--space-6);
        bottom: var(--space-6);
        left: var(--space-6);
    }
}

/* The lines of the ghosts (Ghost::chart(), Ghost::spark()): strokes that
   keep their width as the box stretches, the area fading downwards. */
.ghost-line {
    fill: none;
    stroke: var(--color-ghost-line);
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
}

.ghost-fade-from {
    stop-color: var(--color-ghost-fade);
}

.ghost-fade-to {
    stop-color: var(--color-ghost-fade);
    stop-opacity: 0;
}

.ghost-list {
    padding: var(--space-5) var(--space-5) var(--space-10);
    text-align: center;
}

.ghost-list-rows {
    max-width: 520px;
    margin: 0 auto;
    list-style: none;
    text-align: left;
}

.ghost-list-rows > li {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: 12px 0;
}

.ghost-list-rows > li + li {
    border-top: 1px solid var(--color-border);
}

.ghost-list-end {
    flex: none;
    width: 48px;
}

/* The words come up into the faded last row. */
.ghost-list-body {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-top: calc(-1 * var(--space-5));
}

@media (min-width: 640px) {
    .ghost-list {
        padding-inline: var(--space-6);
    }
}

.ghost-covers {
    display: flex;
    gap: 10px;
    overflow: hidden;
}

.ghost-cover {
    display: flex;
    flex: none;
    flex-direction: column;
    gap: 9px;
    width: var(--ghost-cover-width);
}

.ghost-cover-frame {
    position: relative;
    aspect-ratio: 9 / 16;
    border-radius: var(--radius-sm);
    background: var(--color-ghost);
}

/* Where the plays' pill sits on a real cover. */
.ghost-cover-frame::after {
    content: "";
    position: absolute;
    bottom: 8px;
    left: 8px;
    width: 46px;
    height: 18px;
    border-radius: var(--radius-full);
    background: var(--color-ghost-strong);
}

.ghost-cover > .ghost-bar-sm {
    width: 66%;
}

.ghost-cover:nth-child(3n + 2) > .ghost-bar-sm {
    width: 54%;
}

.ghost-cover:nth-child(3n) > .ghost-bar-sm {
    width: 72%;
}

/* The chip on the middle of the covers, 9 by 16, rather than of the row
   with the handles and the room under it; the row starts at the frame's
   top. */
.ghost-wrap-covers {
    --ghost-cover-width: 112px;
}

.ghost-wrap-covers > .ghost-chip {
    top: calc(var(--ghost-cover-width) * 8 / 9);
}

@media (min-width: 640px) {
    .ghost-covers {
        gap: 12px;
    }

    .ghost-wrap-covers {
        --ghost-cover-width: 124px;
    }
}

.ghost-wrap {
    position: relative;
}

.ghost-chip {
    position: absolute;
    top: 50%;
    left: 50%;
    width: max-content;
    max-width: calc(100% - 2 * var(--space-5));
    padding: 6px 14px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-full);
    background: var(--color-surface);
    box-shadow: var(--shadow-xs);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    line-height: 1.35;
    text-align: center;
    transform: translate(-50%, -50%);
}

.ghost-fade-down {
    -webkit-mask-image: linear-gradient(to bottom, black 0, black 30%, transparent 100%);
    mask-image: linear-gradient(to bottom, black 0, black 30%, transparent 100%);
}

.ghost-fade-right {
    -webkit-mask-image: linear-gradient(to right, black 0, black 35%, transparent 96%);
    mask-image: linear-gradient(to right, black 0, black 35%, transparent 96%);
}

.pagination {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    margin-top: var(--space-4);
}

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

.pagination-links {
    display: flex;
    gap: var(--space-2);
}

@media (max-width: 479px) {
    .pagination {
        flex-direction: column-reverse;
        align-items: stretch;
        text-align: center;
    }

    .pagination-links > .btn {
        flex: 1;
    }
}

/* ------------------------------------------------------------------------
   Dialogs: the window every short form opens in, over the page it was
   asked from, and the confirmation of forms marked data-confirm. app.js
   builds both; a form's window is its dialog page, fetched. [D73]

   The parts, top to bottom:
     .dialog-handle    the bar to grab, on phones only
     .dialog-head      .dialog-title and the X (.dialog-close); it never
                       scrolls
     .dialog-body      the page's content; the one part that scrolls. A
                       confirmation's consequence is its .dialog-message
     .dialog-foot      the action, taken from the form's .form-actions,
                       on the right and at least 120 pixels wide, with any
                       button beside it; it never scrolls and has no line.
                       On its left .form-actions-note says what the action
                       will do, .dialog-note how many fields need
                       attention, .dialog-ask asks before what was typed
                       is discarded.
   A hairline under the head shows once the body is scrolled, and while
   more of the body lies below, its last pixels fade into the foot
   (.is-scrolled, .has-more).

   In MyPeptideApp's manner (D101, D108): the title in the text's face and
   no quiet line under it; labels in ink at the text's size, "optional"
   grey; light fields of 40 pixels (44 under a finger) and a tight rhythm;
   a yes or no as a switch on its row (.switch-row); a segmented choice
   across the window, its parts equal; two actions side by side in the foot
   where a window has two; and no Cancel: the X closes a form's window, as
   its back link leads back from its page. A confirmation keeps Cancel
   beside its action, where the way back must be as plain as the way on;
   one that destroys nothing and asks in one window with one action
   (data-confirm-one, .is-one), publishing in the link page studio, has a
   window's X instead (D117, point 6).

   Wide screens: a window of 520 pixels, 640 for .is-wide, 420 for a
   confirmation (.is-confirm), always in the middle of the screen with as
   much room above it as below, however long it is (the developer, 5
   October 2026). Phones, below 640 pixels: a sheet from the bottom edge,
   with a bar to drag it down by and its buttons side by side.

   It rises a little and fades in over the page, which dims and blurs; a
   sheet slides up. Closing is quicker. Nothing moves for someone who asked
   for less motion.
   ------------------------------------------------------------------------ */

/* How far a sheet is dragged down, 0 to 1, as a number the browser can
   animate back when the sheet springs into place. */
@property --dialog-drag {
    syntax: '<number>';
    inherits: true;
    initial-value: 0;
}

/* Nothing behind a window scrolls. Where scrollbars take room, the page
   behind shifts by their width, under the blur, rather than keeping a
   gutter that the dimmed backdrop would leave out. */
html:has(.dialog[open]) {
    overflow: hidden;
}

.dialog {
    /* The room it keeps from the top and the bottom edge, the same both
       ways, more on a taller screen. */
    --dialog-room: clamp(24px, 6vh, 64px);
    width: min(520px, calc(100vw - 48px));
    max-width: none;
    max-height: min(780px, calc(100vh - 2 * var(--dialog-room)));
    max-height: min(780px, calc(100dvh - 2 * var(--dialog-room)));
    margin: auto;
    padding: 0;
    overflow: hidden;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-dialog);
    background: var(--color-surface);
    color: var(--color-text);
    box-shadow: var(--shadow-dialog);
}

.dialog.is-wide {
    width: min(640px, calc(100vw - 48px));
}

.dialog.is-confirm {
    width: min(420px, calc(100vw - 48px));
}

.dialog[open] {
    display: flex;
    flex-direction: column;
}

/* The window itself takes the focus on a phone, where a field would open
   the keyboard over it at once. Its title says where one is. */
.dialog:focus {
    outline: none;
}

/* The page behind, dimmed and blurred. ::backdrop takes the custom
   properties of its dialog since Chrome 122, Safari 17.4 and Firefox 120;
   an older browser shows the window over the page undimmed. */
.dialog::backdrop {
    background: var(--color-overlay);
    -webkit-backdrop-filter: blur(var(--backdrop-blur));
    backdrop-filter: blur(var(--backdrop-blur));
    opacity: calc(1 - var(--dialog-drag, 0));
}

.dialog-handle {
    display: none;
}

.dialog-head {
    display: flex;
    flex: none;
    align-items: flex-start;
    gap: var(--space-4);
    padding: var(--space-5) var(--space-4) var(--space-4) var(--space-6);
    border-bottom: 1px solid transparent;
    transition: border-color var(--duration-fast) var(--ease-in-out);
}

.dialog.is-scrolled .dialog-head {
    border-bottom-color: var(--color-border);
}

.dialog-heading {
    flex: 1;
    min-width: 0;
    /* The title's first line level with the X, whose height grows to 44
       pixels under a finger. */
    padding-top: calc((var(--control-height-sm) - var(--text-xl) * 1.25) / 2);
}

/* Without an X, the head's padding is even on both sides. */
.dialog.is-confirm .dialog-head {
    padding-right: var(--space-6);
}

.dialog.is-confirm .dialog-heading {
    padding-top: var(--space-1);
}

/* One action and the X in place of Cancel: a window's head again. */
.dialog.is-confirm.is-one .dialog-head {
    padding-right: var(--space-4);
}

.dialog.is-confirm.is-one .dialog-heading {
    padding-top: calc((var(--control-height-sm) - var(--text-xl) * 1.25) / 2);
}

/* In the text's face, Inter, as every title inside a page is: the display
   face is the top of a page's alone (D102, D108, point 1). */
.dialog-title {
    font-family: var(--font-sans);
    font-size: var(--text-xl);
    font-weight: var(--weight-semibold);
    line-height: 1.25;
    letter-spacing: -0.015em;
    text-wrap: balance;
}

/* The consequence a confirmation states, which is what it asks about. */
.dialog-message {
    color: var(--color-text-muted);
    line-height: var(--leading-normal);
    text-wrap: pretty;
}

.dialog-close {
    flex: none;
    color: var(--color-text-subtle);
}

.dialog-body {
    flex: 1 1 auto;
    min-height: 0;
    padding: var(--space-1) var(--space-6) var(--space-6);
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
}

.dialog-body > * + * {
    margin-top: var(--space-5);
}

/* A form of hidden fields alone, whose button stands in the foot (a note's
   Delete beside Save changes, D108 point 5), takes no room: it still sends,
   by its button's form attribute. */
.dialog-body > form:not(:has(:not(input[type="hidden"]))) {
    display: none;
}

.dialog-body > .error-summary {
    margin-bottom: 0;
}

.dialog-body .form {
    max-width: none;
}

.dialog-body .readonly-notice {
    margin-top: var(--space-5);
}

/* While more of the body lies below, its last pixels fade into the foot
   rather than end at a line. */
.dialog.has-more .dialog-body {
    -webkit-mask-image: linear-gradient(to top, transparent 0, black var(--space-6));
    mask-image: linear-gradient(to top, transparent 0, black var(--space-6));
}

/* Inside a window, in MyPeptideApp's manner (D101, D108, point 3): a
   field's label, and a choice's legend, its own name in ink at the size of
   the text, 8 pixels above its field, the word "optional" grey; its help
   grey under it, where what a window must say stands; light fields of 40
   pixels (44 under a finger) with the light border, the small radius and
   no shadow, a step darker under the pointer; 20 pixels between fields,
   the .form's gap. :where() keeps these rules at the weight of the fields'
   own, so the focus, an error, read only and disabled stay every field's.
   The light border is below the 3:1 of a field on a page (tokens.css): in
   a window every field stands under its label, as the agreed mock has
   it. */
:where(.dialog-body .form) .field {
    gap: var(--space-2);
}

:where(.dialog-body .form) .field-label {
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
}

:where(.dialog-body .form) .field-help {
    color: var(--color-text-subtle);
    font-size: var(--text-sm);
}

/* A segmented choice spans the window (D108, point 6): its parts equal and
   slimmer, 34 pixels (38 under a finger), on a soft track without a frame,
   the chosen part raised on it. */
:where(.dialog-body) .segmented {
    display: flex;
    flex-wrap: nowrap;
    width: 100%;
    gap: 0;
    border: 0;
    border-radius: var(--radius-md);
}

:where(.dialog-body) .segment {
    flex: 1 1 0;
    justify-content: center;
    min-width: 0;
    min-height: 34px;
    padding: 0 var(--space-2);
    border-radius: calc(var(--radius-md) - 3px);
    font-size: var(--text-sm);
    text-align: center;
}

@media (pointer: coarse) {
    :where(.dialog-body) .segment {
        min-height: 38px;
    }
}

:where(.dialog-body .form) :is(.input, .select, .input-join) {
    min-height: var(--control-height-md);
}

:where(.dialog-body .form) :is(.input, .select, .textarea, .input-join) {
    border-color: var(--color-border);
    background-color: var(--color-surface);
    box-shadow: none;
}

@media (hover: hover) {
    :where(.dialog-body .form) .input:hover:not(:focus-visible, :disabled, [readonly], [aria-invalid="true"]),
    :where(.dialog-body .form) .textarea:hover:not(:focus-visible, :disabled, [readonly], [aria-invalid="true"]),
    :where(.dialog-body .form) .select:hover:not(:focus-visible, :disabled, [aria-invalid="true"]),
    :where(.dialog-body .form) .input-join:hover:not(:focus-within):not(:has([aria-invalid="true"])) {
        border-color: var(--color-border-strong);
        background-color: var(--color-surface);
    }
}

.dialog-foot {
    display: flex;
    flex: none;
    flex-direction: row-reverse;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-start;
    gap: var(--space-3);
    padding: 0 var(--space-6) var(--space-6);
    background: var(--color-surface);
}

.dialog-foot .btn {
    min-width: 120px;
    min-height: var(--control-height-md);
}

/* "3 fields need your attention.", after an answer with two errors or
   more, "Discard what you entered?", and what the action will do
   (.form-actions-note, the condition window's count): at the start of the
   foot, the buttons after them. The foot runs from the right
   (row-reverse), so these stand last in it and push themselves to its
   left edge. While the foot counts errors, the note gives way to them. */
.dialog-note,
.dialog-ask,
.dialog-foot > .form-actions-note {
    flex: 1 1 auto;
    order: 1;
    min-width: 0;
    margin-right: auto;
}

.dialog-foot:has(> .dialog-note) > .form-actions-note {
    display: none;
}

.dialog-note {
    display: flex;
    align-items: center;
    gap: 6px;
    color: var(--color-danger-text);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
}

.dialog-note .icon {
    flex: none;
    width: 1rem;
    height: 1rem;
}

.dialog-ask {
    font-weight: var(--weight-medium);
}

/* A next step in the same window, and the form again with its errors: a
   short fade, nothing that moves. */
.dialog-fade {
    animation: dialog-fade-in var(--duration-normal) var(--ease-in-out) both;
}

/* The page is on its way after a form went through (app.js). */
.dialog.is-leaving .dialog-body {
    opacity: 0.55;
    transition: opacity var(--duration-fast) var(--ease-in-out) 150ms;
}

@media (hover: hover) {
    .dialog-close:hover {
        background-color: var(--color-surface-muted);
        color: var(--color-text);
    }
}

@media (prefers-reduced-motion: no-preference) {
    .dialog[open] {
        animation: dialog-in 260ms var(--ease-out) backwards;
    }

    .dialog[open]::backdrop {
        animation: dialog-fade-in 220ms var(--ease-in-out) backwards;
    }

    .dialog.is-closing {
        animation: dialog-out 150ms var(--ease-in-out) forwards;
    }

    .dialog.is-closing::backdrop,
    .dialog.is-dismissing::backdrop {
        animation: dialog-fade-out 180ms var(--ease-in-out) forwards;
    }
}

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

    .dialog.is-leaving .dialog-body {
        transition: none;
    }
}

@keyframes dialog-in {
    from { opacity: 0; transform: translateY(12px) scale(0.98); }
}

@keyframes dialog-out {
    to { opacity: 0; transform: translateY(6px) scale(0.985); }
}

@keyframes dialog-fade-in {
    from { opacity: 0; }
}

@keyframes dialog-fade-out {
    to { opacity: 0; }
}

@keyframes dialog-sheet-in {
    from { transform: translateY(100%); }
}

@keyframes dialog-sheet-out {
    to { transform: translateY(100%); }
}

@media (max-width: 639px) {
    .dialog,
    .dialog.is-wide,
    .dialog.is-confirm {
        width: 100%;
        max-height: calc(100vh - 40px);
        max-height: calc(100dvh - env(safe-area-inset-top, 0px) - 40px);
        margin: auto 0 0;
        border-width: 1px 0 0;
        border-radius: var(--radius-dialog) var(--radius-dialog) 0 0;
        box-shadow: var(--shadow-sheet);
    }

    /* The bar to grab, with room around it for a thumb. */
    .dialog-handle {
        display: block;
        position: relative;
        flex: none;
        height: 22px;
        touch-action: none;
        cursor: grab;
    }

    .dialog-handle::after {
        content: '';
        position: absolute;
        top: 10px;
        left: 50%;
        width: 36px;
        height: 4px;
        margin-left: -18px;
        border-radius: var(--radius-full);
        background: var(--color-border-strong);
    }

    .dialog-head,
    .dialog.is-confirm .dialog-head {
        padding: var(--space-1) var(--space-3) var(--space-3) var(--space-5);
        touch-action: none;
    }

    .dialog.is-confirm .dialog-head {
        padding-right: var(--space-5);
    }

    .dialog.is-confirm.is-one .dialog-head {
        padding-right: var(--space-3);
    }

    .dialog-heading,
    .dialog.is-confirm.is-one .dialog-heading {
        padding-top: calc((var(--control-height-sm) - var(--text-lg) * 1.25) / 2);
    }

    .dialog-title {
        font-size: var(--text-lg);
    }

    .dialog-body {
        padding: var(--space-1) var(--space-5) var(--space-6);
    }

    .dialog-foot {
        padding: 0 var(--space-5) calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
    }

    /* Two buttons side by side, as wide as each other; a third goes to a
       row of its own. */
    .dialog-foot .btn {
        flex: 1 1 calc(50% - var(--space-3));
    }

    /* The line goes above the buttons, on a row of its own. */
    .dialog-note,
    .dialog-ask,
    .dialog-foot > .form-actions-note {
        flex-basis: 100%;
        order: -1;
    }

    /* The sheet follows the finger while it is dragged (app.js). */
    .dialog.is-dragging {
        transition: none;
    }

    @media (prefers-reduced-motion: no-preference) {
        .dialog[open] {
            animation: dialog-sheet-in 380ms var(--ease-sheet) backwards;
        }

        .dialog.is-closing {
            animation: dialog-sheet-out 220ms cubic-bezier(0.4, 0, 1, 1) forwards;
        }
    }
}

/* ------------------------------------------------------------------------
   The filter row (D92): one GET form above what it scopes, the same on
   every page that filters (partials/filter-row.php, app.js). See "A filter
   row" in admin_app/views/README.md.

     .filter-row          the form: the period, then .filter-line
     .filter-period       the period, an .an-seg of links (analytics.css),
                          its calendar item .filter-dates at its end
     .filter-line         .filter-track between the arrows .filter-turn,
                          and on a phone the round button .filter-open
                          (.is-wide with its word, .has-set with a dot)
     .filter-track        what follows the period: .filter-pics, the
                          dropdowns .filter-field with .filter-field-label
                          (.is-fit as wide as its longest choice),
                          and on a list the dates item .filter-dates-button;
                          .is-in-next and .is-in-back while a page comes in
     .filter-pics         the pictures: .filter-pic (.is-all, .is-chosen)
                          with .filter-pic-input, .filter-pic-face or
                          .filter-pic-mark, .filter-pic-name
     .filter-pill         the grey pill of the chosen picture or period
     .filter-fallback, .filter-apply
                          the date fields and Apply, without script alone
     .filter-sheet        the phone's Filters sheet, .filter-sheet-fields
     .filter-dates-window the dates window: .filter-ends with .filter-end
                          (.is-active, -label, -value, .is-empty) and
                          .filter-ends-arrow, .filter-months with
                          .filter-month (-head, -title, -nav, -grid),
                          .filter-dow, .filter-blank, .filter-day (.is-today,
                          .is-in, .is-start, .is-end), .filter-days-count,
                          .filter-any

   Desktop: one line after the period, split by app.js into pages of whole
   filters, one shown, the arrows at its ends; without script it wraps.
   Phones, below 640 pixels: the period on a line of its own, its items
   sharing it; under it the pictures and the round Filters button, whose
   sheet holds the dropdowns. Pictures wider than their room scroll
   sideways, on every screen.
   ------------------------------------------------------------------------ */

.filter-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px var(--space-3);
    margin-bottom: var(--space-5);
}

.filter-period {
    flex: none;
}

/* Under the period once it would leave the line less than this. */
.filter-line {
    display: flex;
    flex: 1 1 18rem;
    align-items: center;
    gap: var(--space-3);
    min-width: 0;
}

/* The track keeps 4 pixels round its filters, so a focus ring is not cut
   off where it hides what lies beyond its page. */
.filter-track {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    min-width: 0;
    margin: -4px;
    padding: 4px;
}

@media (scripting: enabled) {
    .filter-track {
        flex-wrap: nowrap;
        overflow: hidden;
    }
}

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

/* In the row a dropdown is named by its empty choice; its label is for a
   screen reader, and for the sheet, which shows it. */
.filter-track .filter-field-label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
}

.filter-track .select {
    width: auto;
    max-width: 12rem;
}

/* A dropdown of choices in words, which 12rem would cut: as wide as its
   longest one, within the line. */
.filter-track .filter-field.is-fit .select {
    max-width: 100%;
}

/* Where the pointer is precise, and so no phone zooms into a small field,
   the dropdowns take the type of the buttons and the period beside them. */
@media (hover: hover) and (pointer: fine) {
    .filter-track .select {
        font-size: var(--text-base);
        font-weight: var(--weight-medium);
    }
}

/* The period's chosen item lies on its pill (below), which glides. */
.filter-period .an-seg-item {
    position: relative;
    isolation: isolate;
}

.filter-period .an-seg-item[aria-current] {
    background: none;
    box-shadow: none;
}

.filter-period .filter-pill {
    border-radius: inherit;
    background: var(--color-surface-raised);
    box-shadow: var(--shadow-sm);
}

/* The calendar item: its icon alone, or the icon and the dates chosen. */
.filter-dates {
    gap: 6px;
}

.filter-dates .icon,
.filter-dates-button .icon {
    width: 17px;
    height: 17px;
}

.filter-dates-button {
    flex: none;
}

/* Without script the plain date fields stand in for the dates window, and
   Apply sends the form; with it neither shows, nor do they take room. */
.filter-fallback,
.filter-apply {
    display: none;
}

.filter-fallback {
    gap: var(--space-3);
}

.filter-fallback .input {
    width: auto;
}

@media (scripting: none) {
    .filter-fallback {
        display: flex;
    }

    .filter-apply {
        display: inline-flex;
    }

    .filter-period > .filter-dates,
    .filter-dates-button,
    .filter-open {
        display: none;
    }
}

/* The arrows to the next page of filters, at the far end, and back, at the
   start: round, as high as a dropdown, with its border. A dot says that a
   filter on the pages behind it is set. */
.filter-turn {
    position: relative;
    display: grid;
    flex: none;
    place-items: center;
    width: var(--control-height);
    height: var(--control-height);
    padding: 0;
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-full);
    background: var(--color-surface);
    color: var(--color-text);
    box-shadow: var(--shadow-xs);
    cursor: pointer;
}

.filter-turn[data-filter-turn="1"] {
    margin-left: auto;
}

.filter-turn .icon,
.filter-open .icon {
    width: 18px;
    height: 18px;
}

.filter-open.has-set::after {
    content: "";
    position: absolute;
    top: 6px;
    right: 6px;
    width: 10px;
    height: 10px;
    border: 2px solid var(--color-surface);
    border-radius: var(--radius-full);
    background: var(--color-text);
}

@media (hover: hover) {
    .filter-turn:hover,
    .filter-open:hover {
        background: var(--color-surface-muted);
    }
}

.filter-track.is-in-next {
    animation: filter-in-next var(--duration-glide) var(--ease-out);
}

.filter-track.is-in-back {
    animation: filter-in-back var(--duration-glide) var(--ease-out);
}

@keyframes filter-in-next {
    from { opacity: 0; transform: translateX(24px); }
}

@keyframes filter-in-back {
    from { opacity: 0; transform: translateX(-24px); }
}

@media (prefers-reduced-motion: reduce) {
    .filter-track.is-in-next,
    .filter-track.is-in-back {
        animation: none;
    }
}

/* The phone's Filters button, round as the arrows, with its word where the
   line has no pictures. */
.filter-open {
    position: relative;
    display: none;
    flex: none;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    width: var(--control-height);
    height: var(--control-height);
    margin-left: auto;
    padding: 0;
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-full);
    background: var(--color-surface);
    color: var(--color-text);
    box-shadow: var(--shadow-xs);
    font: inherit;
    font-weight: var(--weight-medium);
    cursor: pointer;
}

.filter-open:not(.is-wide) .filter-open-text {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
}

.filter-open.is-wide {
    width: auto;
    margin-left: 0;
    padding: 0 var(--space-4);
}

.filter-open.is-wide.has-set::after {
    top: -3px;
    right: -3px;
}

/* ------------------------------------------------------------------------
   The pictures: All, then a round face per model or a logo per platform,
   as high as a dropdown and without a track around them. The chosen one
   lies on a soft grey pill, as the sidebar marks its page, its name beside
   it in semibold; the others show the picture alone, named by their title
   and to a screen reader. A radio group of the form, so the pictures work
   without script, with Apply.
   ------------------------------------------------------------------------ */

/* Wider than their room, the pictures scroll sideways, keeping 4 pixels
   round them as the track does. */
.filter-pics {
    display: flex;
    flex: 0 1 auto;
    align-items: center;
    gap: 2px;
    min-width: 0;
    margin: -4px calc(var(--space-2) - 4px) -4px -4px;
    padding: 4px;
    overflow-x: auto;
    scrollbar-width: none;
}

.filter-pics::-webkit-scrollbar {
    display: none;
}

.filter-pic {
    position: relative;
    isolation: isolate;
    display: inline-flex;
    flex: none;
    align-items: center;
    gap: var(--space-2);
    height: var(--control-height);
    padding: 0 5px;
    border-radius: var(--radius-full);
    color: var(--color-text-muted);
    font-weight: var(--weight-medium);
    white-space: nowrap;
    cursor: pointer;
}

.filter-pic.is-all {
    padding: 0 14px;
}

.filter-pic.is-chosen {
    padding-right: var(--space-4);
    color: var(--color-text);
    font-weight: var(--weight-semibold);
}

.filter-pic.is-all.is-chosen {
    padding: 0 14px;
}

/* The radio, out of sight under its picture; the label shows its focus. */
.filter-pic-input {
    position: absolute;
    inset: 0;
    margin: 0;
    opacity: 0;
    pointer-events: none;
}

.filter-pic:has(.filter-pic-input:focus-visible) {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}

/* A picture ticked and not sent yet, between arrow keys or before Apply. */
.filter-pic:not(.is-chosen):has(.filter-pic-input:checked) {
    box-shadow: inset 0 0 0 1px var(--color-border-strong);
}

@media (hover: hover) {
    .filter-pic:not(.is-chosen):hover {
        background: var(--color-canvas-hover);
        color: var(--color-text);
    }
}

.filter-pill {
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: var(--radius-full);
    background: var(--color-canvas-selected);
}

.filter-pic .filter-pic-face,
.filter-pic-mark {
    display: grid;
    flex: none;
    place-items: center;
    width: 34px;
    height: 34px;
}

.filter-pic-mark {
    border: 1px solid var(--mark-border, var(--color-border));
    border-radius: var(--radius-full);
    background: var(--mark-bg, var(--color-surface-muted));
    color: var(--mark-fg, var(--color-text-muted));
}

.filter-pic-mark svg {
    width: 16px;
    height: 16px;
}

.filter-pic-mark .glyph {
    fill: currentColor;
}

/* The name opens beside the chosen picture; All is its word alone. */
.filter-pic-name {
    max-width: 0;
    margin-left: calc(-1 * var(--space-2));
    overflow: hidden;
    opacity: 0;
}

.filter-pic.is-chosen .filter-pic-name {
    max-width: 9rem;
    margin-left: 0;
    opacity: 1;
    text-overflow: ellipsis;
}

.filter-pic.is-all .filter-pic-name {
    max-width: none;
    margin-left: 0;
    opacity: 1;
}

/* ------------------------------------------------------------------------
   The glide. Choosing another picture or period loads a new page; a
   cross-document view transition carries the grey pill from the old
   chosen item to the new one while the items between move to their places
   and the new name opens. app.js keeps it for the navigations its filter
   row starts and skips every other (pageswap). Each item is named by its
   place, which a filter's next page keeps; the pill lies under them. The
   page itself is not captured, so it switches as without a transition.
   Nothing glides with reduced motion, or in a browser without it.
   ------------------------------------------------------------------------ */

@media (prefers-reduced-motion: no-preference) {
    @view-transition {
        navigation: auto;
    }
}

:root {
    view-transition-name: none;
}

.filter-period > :nth-child(1) { view-transition-name: filter-period-1; }
.filter-period > :nth-child(2) { view-transition-name: filter-period-2; }
.filter-period > :nth-child(3) { view-transition-name: filter-period-3; }
.filter-period > :nth-child(4) { view-transition-name: filter-period-4; }
.filter-period > :nth-child(5) { view-transition-name: filter-period-5; }
.filter-pic:nth-of-type(1) { view-transition-name: filter-pic-1; }
.filter-pic:nth-of-type(2) { view-transition-name: filter-pic-2; }
.filter-pic:nth-of-type(3) { view-transition-name: filter-pic-3; }
.filter-pic:nth-of-type(4) { view-transition-name: filter-pic-4; }
.filter-pic:nth-of-type(5) { view-transition-name: filter-pic-5; }
.filter-pic:nth-of-type(6) { view-transition-name: filter-pic-6; }
.filter-pic:nth-of-type(7) { view-transition-name: filter-pic-7; }
.filter-pic:nth-of-type(8) { view-transition-name: filter-pic-8; }
.filter-pic:nth-of-type(9) { view-transition-name: filter-pic-9; }
.filter-pic:nth-of-type(10) { view-transition-name: filter-pic-10; }
.filter-pic:nth-of-type(11) { view-transition-name: filter-pic-11; }
.filter-pic:nth-of-type(12) { view-transition-name: filter-pic-12; }

.filter-period > *,
.filter-pic {
    view-transition-class: filter-item;
}

.filter-period .filter-pill {
    view-transition-name: filter-period-pill;
    view-transition-class: filter-pill;
}

.filter-pics .filter-pill {
    view-transition-name: filter-pic-pill;
    view-transition-class: filter-pill;
}

/* A row in the record panel, an account's numbers over the Accounts tab,
   names nothing: a name is the page's own row's alone, as two of one name
   would skip the transition, and the panel's filters load in place. */
.side-panel .filter-period > *,
.side-panel .filter-pic,
.side-panel .filter-pill {
    view-transition-name: none;
}

::view-transition-group(*.filter-item),
::view-transition-group(*.filter-pill) {
    animation-duration: var(--duration-glide);
    animation-timing-function: var(--ease-out);
}

/* Each item above the pill, cut to its box as it narrows or widens: the
   picture stays where it is and the name opens or closes beside it, never
   stretched. */
::view-transition-group(*.filter-item) {
    z-index: 1;
    overflow: clip;
}

::view-transition-old(*.filter-item),
::view-transition-new(*.filter-item) {
    width: auto;
    height: 100%;
    animation-duration: var(--duration-glide);
}

/* ------------------------------------------------------------------------
   The phone: the period across its line, the pictures and the Filters
   button under it. The dropdowns wait in the sheet, which app.js moves
   them into; until it has, they stay out of sight.
   ------------------------------------------------------------------------ */

@media (max-width: 639px) {
    .filter-row > .filter-period {
        display: flex;
        width: 100%;
    }

    .filter-period .an-seg-item {
        flex: 1 1 auto;
        justify-content: center;
        padding: 0 6px;
    }

    .filter-period .filter-dates[aria-current] {
        flex: none;
        padding: 0 10px;
    }

    .filter-line {
        flex-basis: 100%;
    }

    .filter-pics {
        margin-right: -4px;
    }

    .filter-turn {
        display: none;
    }
}

@media (max-width: 639px) and (scripting: enabled) {
    .filter-track > .filter-field {
        display: none;
    }

    /* With every dropdown in the Filters sheet, a track that held nothing
       else would only keep its room before the Filters button. */
    .filter-track:not(:has(> :not(.filter-field))) {
        display: none;
    }

    .filter-open {
        display: inline-flex;
    }
}

/* The sheet: every dropdown of the row with its label over it. Open again
   on the page a change in it caused, it stands as if it had stayed, its
   backdrop too (.is-still; app.js stops the sheet's own rise). */
.filter-sheet.is-still:not(.is-closing, .is-dismissing)::backdrop {
    animation: none;
}

.filter-sheet-fields {
    display: grid;
    gap: var(--space-4);
}

.filter-sheet .filter-field-label {
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
}

.filter-sheet .select {
    width: 100%;
}

/* ------------------------------------------------------------------------
   The dates window: From and To on top, the one being picked outlined; two
   months on a wide screen, one on a phone; the days between the two ends
   on a band, and how many there are under it. The first tap is From, the
   second To, and the dates apply at once.
   ------------------------------------------------------------------------ */

@media (min-width: 640px) {
    .dialog.filter-dates-window {
        width: min(624px, calc(100vw - 48px));
    }
}

.filter-ends {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    align-items: center;
    gap: var(--space-2);
}

.filter-end {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.filter-end:disabled {
    cursor: default;
}

.filter-end.is-active {
    border-color: var(--color-focus);
    box-shadow: 0 0 0 1px var(--color-focus);
}

.filter-end-label {
    color: var(--color-text-subtle);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
}

.filter-end-value {
    overflow: hidden;
    font-weight: var(--weight-medium);
    white-space: nowrap;
    text-overflow: ellipsis;
}

.filter-end-value.is-empty {
    color: var(--color-text-subtle);
    font-weight: var(--weight-normal);
}

.filter-ends-arrow {
    display: grid;
    color: var(--color-text-subtle);
}

.filter-ends-arrow .icon {
    width: 16px;
    height: 16px;
}

.filter-months {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-6);
}

.filter-month-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    min-height: var(--control-height-sm);
    margin-bottom: var(--space-2);
}

.filter-month-title {
    flex: 1;
    font-weight: var(--weight-semibold);
    text-align: center;
}

.filter-month-nav {
    flex: none;
    width: var(--control-height-sm);
}

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

.filter-dow {
    padding: 6px 0;
    color: var(--color-text-subtle);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    text-align: center;
}

.filter-blank {
    height: 38px;
}

.filter-day {
    position: relative;
    display: grid;
    place-items: center;
    height: 38px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--color-text);
    font: inherit;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
}

.filter-day span {
    position: relative;
    z-index: 1;
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    border-radius: var(--radius-full);
}

.filter-day:focus-visible {
    outline: none;
}

.filter-day:focus-visible span {
    outline: 2px solid var(--color-focus);
    outline-offset: 1px;
}

@media (hover: hover) {
    .filter-day:not(:disabled):hover span {
        background: var(--color-canvas-selected);
    }
}

.filter-day:disabled {
    color: var(--color-text-subtle);
    cursor: default;
}

.filter-day.is-today span {
    box-shadow: inset 0 0 0 1px var(--color-border-strong);
}

/* The band between the two ends, half a day wide at each. */
.filter-day.is-in::before,
.filter-day.is-start::before,
.filter-day.is-end::before {
    content: "";
    position: absolute;
    inset: 1px 0;
    background: var(--color-surface-muted);
}

.filter-day.is-start::before {
    left: 50%;
}

.filter-day.is-end::before {
    right: 50%;
}

.filter-day.is-start.is-end::before {
    display: none;
}

.filter-day.is-start span,
.filter-day.is-end span,
.filter-day.is-start:hover span,
.filter-day.is-end:hover span {
    background: var(--color-text);
    color: var(--color-surface);
    font-weight: var(--weight-semibold);
}

.filter-days-count {
    min-height: 1.5em;
    color: var(--color-text-subtle);
    font-size: var(--text-sm);
    text-align: center;
}

.filter-any {
    display: flex;
    justify-content: center;
}

@media (max-width: 639px) {
    .filter-months {
        grid-template-columns: minmax(0, 1fr);
    }

    .filter-day,
    .filter-blank {
        height: 42px;
    }

    .filter-day span {
        width: 40px;
        height: 40px;
    }
}

/* ------------------------------------------------------------------------
   The way to a live link (D80): the stepper, the control of two parts, the
   domain menu, the account a link is for, and the window that says a link
   is live. Black, white and grey, but the one green check; nothing moves
   but the check, once.

     .steps, .step, .step-bar, .step-label
                         the steps of a flow, the setup's four or the own
                         domain's three, a bar and a label each: done and
                         current in ink, later grey; in a window
                         .dialog-steps holds them (app.js)
     .input-join, .input-join-select, .input-join-domain,
     .input-join-prefix, .input-join-slash, .input-join-input
                         one control of two parts: a platform and its
                         handle, a domain and the ending of an address,
                         framed once and focused once, as a text field
     .link-preview       the whole address under its control
     .menu, .menu-item, .menu-item-text, .menu-item-name, .menu-item-line,
     .menu-check, .menu-divider
                         a small menu in the look of the account menu: the
                         domains, an own one that cannot be chosen now grey
                         with why, and "Connect own domain" (D89)
     .fixed-row          a choice with one answer, shown as a row
     .field-note         a quiet line under a field that says why
     .field-label-text   a label over something that is no field
     .done, .done-mark, .done-check, .done-ring, .done-title,
     .done-copy, .done-address, .done-line
                         a link is live: the green check that plays once,
                         the address with Copy, where to paste it
     .text-button        a quiet button inside a line of text
     .lp-list, .lp-row and its parts
                         a link page as a row of the list of link pages
   ------------------------------------------------------------------------ */

.steps {
    display: grid;
    grid-auto-columns: minmax(0, 1fr);
    grid-auto-flow: column;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
}

.step {
    min-width: 0;
}

.step-bar {
    display: block;
    height: 3px;
    border-radius: var(--radius-full);
    background: var(--color-border);
}

.step.is-done .step-bar,
.step.is-current .step-bar {
    background: var(--color-primary);
}

.step-label {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-top: 8px;
    overflow: hidden;
    color: var(--color-text-subtle);
    font-size: var(--text-xs);
    line-height: 16px;
    white-space: nowrap;
}

.step-label > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

.step.is-done .step-label {
    color: var(--color-text-muted);
}

.step.is-current .step-label {
    color: var(--color-text);
    font-weight: var(--weight-medium);
}

.step-label .icon {
    flex: none;
    width: 12px;
    height: 12px;
    stroke-width: 2.75;
}

/* A dialog page loaded as a page of its own: the steps above its title. */
.container > .steps {
    max-width: 520px;
    margin-bottom: var(--space-6);
}

/* In a window: above the title. On a wide screen level with the X, on a
   phone a row of its own under the bar, so the four labels keep their
   room. A window without steps has the slot empty and hidden. */
.dialog-steps {
    display: none;
}

.dialog.has-steps .dialog-steps {
    display: block;
}

@media (min-width: 640px) {
    .dialog.has-steps .dialog-steps {
        position: absolute;
        top: calc(var(--space-5) + (var(--control-height-sm) - 27px) / 2);
        right: calc(2 * var(--space-4) + var(--control-height-sm));
        left: var(--space-6);
    }

    .dialog.has-steps .dialog-heading {
        padding-top: calc((var(--control-height-sm) - 27px) / 2 + 27px + var(--space-5));
    }
}

@media (max-width: 639px) {
    .dialog.has-steps .dialog-steps {
        flex: none;
        padding: var(--space-1) var(--space-5) var(--space-2);
        touch-action: none;
    }
}

/* A window whose title stands in its body, under the check: its head holds
   the X alone. On a phone its Done goes across the sheet and its bar drags
   it, so no X stands over the check. With the steps of the setup above it
   the head stays as it is, the X on every screen, and leaves the check the
   room of a title above it. */
.dialog.is-bare-head:not(.has-steps) .dialog-head {
    padding-bottom: 0;
}

@media (max-width: 639px) {
    .dialog.is-bare-head:not(.has-steps) .dialog-head {
        display: none;
    }

    .dialog.is-bare-head:not(.has-steps) .dialog-body {
        padding-top: var(--space-4);
    }
}

/* A window that only asks: its head and its foot. */
.dialog-body:empty {
    display: none;
}

/* A window that only says something: the empty state's look, inside. */
.dialog-body > .empty-state {
    padding: var(--space-6) 0 var(--space-4);
}

/* One control of two parts. The parts show no focus and no border of their
   own: the group shows it once, as a focused text field does, whichever
   part holds it, and turns red while one part is marked invalid. */
.input-join {
    display: flex;
    align-items: stretch;
    min-width: 0;
    min-height: var(--control-height);
    border: 1px solid var(--color-control-border);
    border-radius: var(--radius-sm);
    background: var(--color-input);
    box-shadow: var(--shadow-xs);
    transition: border-color var(--duration-fast) var(--ease-in-out),
                box-shadow var(--duration-fast) var(--ease-in-out);
}

@media (hover: hover) {
    .input-join:hover:not(:focus-within):not(:has([aria-invalid="true"])) {
        border-color: var(--color-control-border-hover);
    }
}

.input-join:focus-within {
    border-color: var(--color-focus);
    box-shadow: 0 0 0 3px var(--color-focus-halo);
}

.input-join:has([aria-invalid="true"]) {
    border-color: var(--color-danger-text);
}

.input-join:has([aria-invalid="true"]):focus-within {
    box-shadow: 0 0 0 3px var(--color-danger-border);
}

.input-join-select,
.input-join-domain {
    flex: none;
    margin: 0;
    border: 0;
    border-radius: calc(var(--radius-sm) - 1px) 0 0 calc(var(--radius-sm) - 1px);
    outline: none;
    background-color: var(--color-surface-muted);
    color: var(--color-text);
    font: inherit;
    font-size: var(--text-md);
    font-weight: var(--weight-medium);
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-in-out);
}

.input-join-select {
    width: 8.75rem;
    max-width: 50%;
    padding: 0 32px 0 var(--space-3);
    background-image: var(--image-select-chevron);
    background-position: right 9px center;
    background-repeat: no-repeat;
    background-size: 16px;
    -webkit-appearance: none;
    appearance: none;
    text-overflow: ellipsis;
}

/* The domain, a select without script, the menu's button with it. */
.input-join-domain {
    font-family: var(--font-mono);
    font-size: 15px;
    font-weight: var(--weight-normal);
    white-space: nowrap;
}

select.input-join-domain {
    width: auto;
    max-width: 60%;
}

button.input-join-domain {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 0 8px 0 var(--space-3);
}

button.input-join-domain .icon {
    width: 16px;
    height: 16px;
    color: var(--color-text-subtle);
}

/* "Choose a domain" before one is chosen: words, not an address. */
button.input-join-domain.is-empty {
    font-family: inherit;
    font-size: var(--text-md);
    font-weight: var(--weight-medium);
}

button.input-join-domain[aria-expanded="true"] {
    background-color: var(--color-surface-selected);
}

@media (hover: hover) {
    .input-join-select:hover,
    .input-join-domain:hover {
        background-color: var(--color-surface-selected);
    }
}

.input-join-select:focus,
.input-join-select:focus-visible,
.input-join-domain:focus,
.input-join-domain:focus-visible,
.input-join-input:focus,
.input-join-input:focus-visible {
    outline: none;
    box-shadow: none;
}

.input-join-prefix,
.input-join-slash {
    display: flex;
    flex: none;
    align-items: center;
    padding-left: var(--space-3);
    color: var(--color-text-subtle);
    font-size: var(--text-md);
    user-select: none;
}

.input-join-slash {
    padding-left: 10px;
    font-family: var(--font-mono);
    font-size: 15px;
}

.input-join-input {
    flex: 1 1 auto;
    width: 100%;
    min-width: 0;
    padding: 0 var(--space-3);
    border: 0;
    border-radius: 0 calc(var(--radius-sm) - 1px) calc(var(--radius-sm) - 1px) 0;
    background: transparent;
    color: var(--color-text);
    font-size: var(--text-md);
    line-height: 1.5;
    /* "Just the domain" beside a long domain on a phone ends in an ellipsis. */
    text-overflow: ellipsis;
}

.input-join-prefix + .input-join-input,
.input-join-slash + .input-join-input {
    padding-left: 2px;
}

.input-join-input.mono {
    font-size: 15px;
}

.input-join-input::placeholder {
    color: var(--color-text-subtle);
    opacity: 1;
}

/* An address as it will be copied, under its control. */
.link-preview {
    margin-top: 2px;
    overflow-wrap: anywhere;
    color: var(--color-text-muted);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
}

/* A small menu in the look of the account menu, placed under its button
   by app.js, inside the window so it stands in the top layer with it. */
.menu {
    position: fixed;
    z-index: 5;
    width: 264px;
    max-width: calc(100vw - 32px);
    margin: 0;
    padding: var(--space-2);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    box-shadow: var(--shadow-lg);
}

.menu[hidden] {
    display: none;
}

.menu-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    min-height: 36px;
    padding: 6px var(--space-3);
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--color-text);
    font: inherit;
    font-weight: 450;
    text-align: left;
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-in-out);
}

@media (hover: hover) {
    .menu-item:hover {
        background: var(--color-surface-hover);
    }
}

.menu-item:focus-visible {
    outline: none;
    background: var(--color-surface-hover);
    box-shadow: inset 0 0 0 2px var(--color-focus);
}

.menu-item-text {
    display: flex;
    flex: 1;
    flex-direction: column;
    min-width: 0;
}

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

.menu-item-name.mono {
    font-size: var(--text-base);
}

.menu-item-line {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

.menu-check {
    flex: none;
    width: 16px;
    height: 16px;
    color: var(--color-text);
}

.menu-divider {
    height: 1px;
    margin: var(--space-2) 0;
    border: 0;
    background: var(--color-border);
}

/* An entry that cannot be taken now, an own domain still checking: grey,
   its line saying why. A link or a button for an action in a menu of a
   page, such as the menu of an own domain, looks like any entry. */
.menu-item:disabled,
.menu-item[aria-disabled="true"] {
    color: var(--color-text-subtle);
    cursor: not-allowed;
}

.menu-item:disabled .menu-item-line,
.menu-item[aria-disabled="true"] .menu-item-line {
    color: var(--color-text-subtle);
}

@media (hover: hover) {
    .menu-item:disabled:hover,
    .menu-item[aria-disabled="true"]:hover {
        background: none;
    }
}

a.menu-item {
    text-decoration: none;
}

@media (pointer: coarse) {
    .menu-item {
        min-height: 44px;
    }
}

/* A choice with one answer: the account a link is for. */
.fixed-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-height: var(--control-height);
    padding: 6px var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface-muted);
}

.fixed-row:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}

.fixed-row[aria-invalid="true"] {
    border-color: var(--color-danger-text);
}

.fixed-row-text {
    display: flex;
    flex: 1;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 var(--space-2);
    min-width: 0;
}

.fixed-row-name {
    font-weight: var(--weight-medium);
    overflow-wrap: anywhere;
}

.fixed-row-line {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

/* A quiet line under a field that says why, once. */
.field-note {
    display: flex;
    align-items: flex-start;
    gap: 6px;
    margin-top: 2px;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    line-height: 1.45;
    text-wrap: pretty;
}

.field-note .icon {
    flex: none;
    width: 15px;
    height: 15px;
    margin-top: 2px;
    color: var(--color-text-subtle);
}

/* A label over something that is no field of its own. */
.field-label-text {
    color: var(--color-text);
    font-size: var(--text-base);
    font-weight: var(--weight-medium);
}

/* A link is live. The green check, 72 pixels, pops in with a soft
   overshoot, the check draws itself, one ring goes out and fades; then the
   title, the address and its line rise in, one after the other. For
   someone who asked for less motion all of it stands still. */
.done {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: var(--space-6) 0 var(--space-2);
    text-align: center;
}

.done-mark {
    position: relative;
    display: grid;
    place-items: center;
    width: 72px;
    height: 72px;
    border-radius: var(--radius-full);
    background: var(--color-success-solid);
    color: var(--color-on-success);
}

.done-check {
    width: 36px;
    height: 36px;
}

.done-ring {
    position: absolute;
    inset: 0;
    border: 2px solid var(--color-success-solid);
    border-radius: inherit;
    opacity: 0;
    pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
    .done-mark {
        animation: done-pop 400ms cubic-bezier(0.34, 1.56, 0.64, 1) 80ms both;
    }

    .done-check path {
        stroke-dasharray: 1;
        stroke-dashoffset: 1;
        animation: done-draw 350ms cubic-bezier(0.65, 0, 0.35, 1) 420ms forwards;
    }

    .done-ring {
        animation: done-ripple 600ms cubic-bezier(0.16, 1, 0.3, 1) 720ms forwards;
    }

    .done-title,
    .done-copy,
    .done-line {
        animation: done-rise 320ms var(--ease-out) both;
    }

    .done-title {
        animation-delay: 300ms;
    }

    .done-copy {
        animation-delay: 360ms;
    }

    .done-line {
        animation-delay: 420ms;
    }
}

@keyframes done-pop {
    from { opacity: 0; transform: scale(0.5); }
    to { opacity: 1; transform: scale(1); }
}

@keyframes done-draw {
    to { stroke-dashoffset: 0; }
}

@keyframes done-ripple {
    0% { opacity: 0.45; transform: scale(1); }
    100% { opacity: 0; transform: scale(1.75); }
}

@keyframes done-rise {
    from { opacity: 0; transform: translateY(6px); }
}

.done-title {
    margin-top: var(--space-5);
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-weight: var(--weight-semibold);
    line-height: 1.25;
    letter-spacing: -0.015em;
    text-wrap: balance;
}

.done-copy {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    margin-top: var(--space-5);
    padding: 6px 6px 6px var(--space-4);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-md);
    background: var(--color-surface-muted);
    text-align: left;
}

/* It breaks only after its slash (<wbr>), never inside the domain or the
   ending: a long one is set a size smaller, by its longer part. */
.done-address {
    flex: 1;
    min-width: 0;
    overflow-wrap: normal;
    color: var(--color-text);
    font-family: var(--font-mono);
    font-size: 17px;
    line-height: 1.4;
}

.done-address.is-longer {
    font-size: var(--text-sm);
}

.done-copy .btn {
    flex: none;
}

/* Copying failed: the address shows whole and selected, and the reason
   goes to a line of its own under it. */
.done-copy:has(.copy-status) {
    flex-wrap: wrap;
}

.done-copy .copy-status {
    flex-basis: 100%;
    margin: 0;
}

.done-line {
    max-width: 36ch;
    margin-top: var(--space-4);
    color: var(--color-text-muted);
    text-wrap: balance;
}

@media (max-width: 639px) {
    .done-address {
        font-size: var(--text-md);
    }

    .done-address.is-long {
        font-size: var(--text-sm);
    }

    .done-address.is-longer {
        font-size: var(--text-xs);
    }
}

/* A quiet button inside a line of text: Add or Replace a tracking link.
   A link looks exactly like the button: no underline, its weight and the
   muted colour, the text colour under the pointer. */
.text-button {
    display: inline-flex;
    align-items: center;
    min-height: 28px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--color-text-muted);
    font: inherit;
    font-weight: var(--weight-medium);
    text-decoration: none;
    cursor: pointer;
}

@media (hover: hover) {
    .text-button:hover {
        color: var(--color-text);
    }
}

/* A link page as a row: its thumbnail, its name and model, its first link
   with Copy (with links.view), its status and a chevron. The whole row leads
   to the editor; the Copy stays above the row's link. On a phone the link
   goes to a line of its own under the name. */
.lp-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.lp-row {
    position: relative;
    display: grid;
    grid-template-columns: 40px minmax(0, 1fr) auto;
    grid-template-areas:
        "thumb main status"
        "thumb links links";
    align-items: center;
    gap: 2px var(--space-3);
    min-height: 72px;
    padding: var(--space-3) var(--space-5);
    border-top: 1px solid var(--color-border);
}

.lp-row:first-child {
    border-top: 0;
}

.lp-row.is-bare {
    grid-template-areas: "thumb main status";
}

@media (hover: hover) {
    .lp-row:hover {
        background: var(--color-surface-hover);
    }
}

.lp-row:has(.lp-name:focus-visible) {
    outline: 2px solid var(--color-focus);
    outline-offset: -2px;
}

/* As tall as a photo whatever it holds, so every row is as high. */
.lp-thumb {
    grid-area: thumb;
    display: grid;
    place-items: center;
    align-self: center;
    height: 52px;
}

.lp-photo {
    display: block;
    width: 40px;
    height: 52px;
    overflow: hidden;
    border-radius: var(--radius-sm);
    background: var(--color-surface-muted);
}

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

.lp-thumb .inv-avatar {
    width: 40px;
    height: 40px;
}

.lp-main {
    grid-area: main;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 var(--space-2);
    min-width: 0;
}

.lp-name {
    color: inherit;
    font-weight: var(--weight-medium);
    text-decoration: none;
    overflow-wrap: anywhere;
}

.lp-name:focus-visible {
    outline: none;
}

/* The whole row opens the editor. */
.lp-name::after {
    content: '';
    position: absolute;
    inset: 0;
}

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

.lp-main .badge {
    align-self: center;
}

.lp-links {
    grid-area: links;
    position: relative;
    z-index: 1;
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 0 var(--space-2);
    min-width: 0;
    max-width: 100%;
    justify-self: start;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

/* The first address on one line, cut with an ellipsis where it runs out
   of room, so Copy and "+N" stay on its line. Copying failed, the whole
   address shows and the reason goes under it. */
.lp-links > [data-copy-text] {
    flex-wrap: nowrap;
    min-width: 0;
}

.lp-links .mono {
    min-width: 0;
    overflow: hidden;
    color: var(--color-text);
    font-size: var(--text-sm);
    text-overflow: ellipsis;
    white-space: nowrap;
}

.lp-links .btn-sm,
.lp-more {
    flex: none;
}

.lp-links > [data-copy-text]:has(.copy-status) {
    flex-wrap: wrap;
}

.lp-links > [data-copy-text]:has(.copy-status) .mono {
    overflow-wrap: anywhere;
    white-space: normal;
}

.lp-links .copy-status {
    flex-basis: 100%;
    margin: 0;
}

.lp-links .btn-sm {
    min-height: 28px;
    padding: 0 var(--space-2);
}

.lp-more {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
}

.lp-status {
    grid-area: status;
    justify-self: end;
}

.lp-chevron {
    display: none;
    color: var(--color-text-subtle);
}

@media (min-width: 640px) {
    .lp-row {
        padding-inline: var(--space-6);
    }
}

@media (min-width: 768px) {
    .lp-row {
        grid-template-columns: 40px minmax(0, 1fr) minmax(0, 1.4fr) 156px 20px;
        grid-template-areas: "thumb main links status chevron";
        gap: var(--space-4);
    }

    .lp-row.is-bare {
        grid-template-columns: 40px minmax(0, 1fr) 156px 20px;
        grid-template-areas: "thumb main status chevron";
    }

    .lp-status {
        justify-self: start;
    }

    .lp-chevron {
        display: block;
        grid-area: chevron;
        width: 18px;
        height: 18px;
    }
}

/* ------------------------------------------------------------------------
   A model's own domain (D89), after the agreed mock
   (docs/mockups/own-domain.html): the card of a model's page, the windows
   that connect, check and move, and their drawings of Cloudflare. Black,
   white and grey; green only for a check that passed and for done, red
   only for a domain that stopped working and a check that failed. Nothing
   moves but the window's rise and the one green check of D80.

     .own-list, .own-row, .own-thumb, .own-main, .own-host, .own-line,
     .own-action, .own-more, .own-ghost-host, .own-ghost-text
                         the card "Own domain": one row per domain in the
                         look of a link page's row, or its ghost; its menu
                         a <details> (data-row-menu) with a .menu
     .own-preview, .own-buy, .own-how
                         the step Domain: the address to come, where to
                         buy, the three lines of how
     .own-shot, .own-shot-label, .own-screen, .own-path, .own-screen-head,
     .own-fake-*, .own-form, .own-f, .own-f-*, .own-switch
                         a drawing of Cloudflare in our own greys, its own
                         words and no mark of its own, a label naming where
     .own-steps, .own-num, .own-step-text, .own-record, .own-quiet, .own-alt,
     .own-steps-text     the step Record: three numbered lines, the record
                         as four rows, the records of another provider, the
                         steps shown to copy by hand when copying failed
     .own-checks, .own-check, .own-mark
                         the step Check: three checks and their state
     .done.is-still      the green check without its animation
     .own-move-from, .own-move-to, .own-bare, .own-bare-none
                         the move: a link's address and the one it gets,
                         and who gets the domain alone
     .own-bios, .own-bio, .own-bio-mark
                         the bios to change, one by one
     .link-suggest       "Use <handle>" under the ending, after the domain
                         alone was refused
   ------------------------------------------------------------------------ */

/* The card's row: the domain's mark, its name with its state and line, the
   one action, and its menu. On a phone the action goes under the line. */
.own-list {
    padding: 0;
    list-style: none;
}

.own-row {
    display: grid;
    grid-template-columns: 40px minmax(0, 1fr) auto;
    grid-template-areas:
        "thumb main more"
        "thumb action action";
    align-items: center;
    gap: 2px var(--space-3);
    min-height: 72px;
    padding: var(--space-3) var(--space-5);
    border-top: 1px solid var(--color-border);
}

.own-row:first-child {
    border-top: 0;
}

.own-row.no-more {
    grid-template-areas:
        "thumb main main"
        "thumb action action";
}

.own-thumb {
    grid-area: thumb;
    display: grid;
    place-items: center;
}

.own-thumb .inv-mark,
.own-thumb .ghost-dot {
    width: 40px;
    height: 40px;
}

.own-thumb .inv-mark .icon {
    width: 18px;
    height: 18px;
}

.own-main {
    grid-area: main;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 2px var(--space-2);
    min-width: 0;
}

.own-host {
    min-width: 0;
    font-family: var(--font-mono);
    font-size: 15px;
    font-weight: var(--weight-medium);
    overflow-wrap: anywhere;
}

.own-line {
    flex-basis: 100%;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    text-wrap: pretty;
}

.own-line.is-problem {
    display: flex;
    align-items: flex-start;
    gap: 6px;
    color: var(--color-danger-text);
    font-weight: var(--weight-medium);
}

.own-line .icon {
    flex: none;
    width: 15px;
    height: 15px;
    margin-top: 2px;
}

.own-action {
    grid-area: action;
    justify-self: start;
    margin-top: var(--space-2);
}

.own-more {
    grid-area: more;
    align-self: center;
}

/* The menu's button: the ghost icon button, without the marker. */
.own-more > summary {
    list-style: none;
    color: var(--color-text-subtle);
}

.own-more > summary::-webkit-details-marker {
    display: none;
}

.own-more[open] > summary {
    background: var(--color-surface-selected);
    color: var(--color-text);
}

/* Without script the menu stands where it is, under its button; with it,
   app.js places it under the button's right edge (data-row-menu). */
.own-more > .menu {
    width: 220px;
}

.own-more > .menu form {
    display: contents;
}

/* The ghost: the shape of an address, the words under it. [D77] */
.own-ghost-host {
    color: var(--color-ghost-line);
    font-family: var(--font-mono);
    font-size: 15px;
    font-weight: var(--weight-medium);
    user-select: none;
}

.own-ghost-text {
    flex-basis: 100%;
    color: var(--color-text-subtle);
    font-size: var(--text-sm);
}

@media (max-width: 639px) {
    .own-row:not(.is-ghost) .own-thumb {
        align-self: start;
        margin-top: 2px;
    }
}

@media (min-width: 640px) {
    .own-row {
        grid-template-columns: 40px minmax(0, 1fr) auto auto;
        grid-template-areas: "thumb main action more";
        gap: var(--space-4);
        padding-inline: var(--space-6);
    }

    .own-row.no-more {
        grid-template-columns: 40px minmax(0, 1fr) auto;
        grid-template-areas: "thumb main action";
    }

    .own-action {
        justify-self: end;
        margin-top: 0;
    }
}

/* The step Domain. */
.own-preview {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    overflow-wrap: anywhere;
}

.own-preview .mono {
    color: var(--color-text);
}

.own-buy {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    line-height: 1.5;
}

.own-buy a {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    color: var(--color-text);
    font-weight: var(--weight-medium);
    white-space: nowrap;
}

.own-buy a .icon {
    width: 14px;
    height: 14px;
    color: var(--color-text-subtle);
}

/* The window's blocks keep its own gap (.dialog-body > * + *); the way to
   buy belongs to its line, close under it, and its body to its summary. */
.dialog-body > .own-buy + .disclosure {
    margin-top: var(--space-1);
}

.own-disclosure-body {
    display: grid;
    gap: var(--space-3);
}

.own-how {
    display: grid;
    gap: 6px;
    padding: 0;
    list-style: none;
    counter-reset: own-how;
    font-size: var(--text-sm);
}

.own-how > li {
    display: grid;
    grid-template-columns: 20px minmax(0, 1fr);
    gap: var(--space-2);
    counter-increment: own-how;
}

.own-how > li::before {
    content: counter(own-how);
    color: var(--color-text-subtle);
    font-variant-numeric: tabular-nums;
    font-weight: var(--weight-medium);
}

.own-how b {
    font-weight: var(--weight-semibold);
}

/* A drawing of Cloudflare: our own greys, its words, no mark of its own.
   Decoration, which the figure's label describes in words. */
.own-shot {
    padding: var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface-muted);
}

.own-shot-label {
    display: block;
    margin: 0 0 var(--space-2) 2px;
    color: var(--color-text-subtle);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
}

.own-screen {
    padding: var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    box-shadow: var(--shadow-xs);
    color: var(--color-text);
    font-size: 12px;
    line-height: 1.35;
}

/* Where the page is in Cloudflare, as its own header shows it. */
.own-path {
    margin-bottom: var(--space-3);
    color: var(--color-text-muted);
    font-size: 12px;
}

.own-path span {
    margin: 0 6px;
    color: var(--color-text-subtle);
}

.own-path b {
    color: var(--color-text);
    font-weight: var(--weight-semibold);
}

.own-screen-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--color-border);
}

.own-screen-head .own-path {
    margin-bottom: 0;
}

.own-fake-search {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 34px;
    padding: 0 4px 0 10px;
    border: 1px solid var(--color-control-border);
    border-radius: var(--radius-sm);
}

.own-fake-search .icon {
    flex: none;
    width: 14px;
    height: 14px;
    color: var(--color-text-subtle);
}

.own-fake-search > span {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    font-family: var(--font-mono);
    text-overflow: ellipsis;
    white-space: nowrap;
}

.own-fake-button {
    display: inline-flex;
    flex: none;
    align-items: center;
    min-height: 26px;
    padding: 0 12px;
    border: 1px solid var(--color-border-strong);
    border-radius: calc(var(--radius-sm) - 2px);
    background: var(--color-surface);
    color: var(--color-text);
    font-weight: var(--weight-semibold);
}

.own-fake-button.is-dark {
    border-color: var(--color-primary);
    background: var(--color-primary);
    color: var(--color-on-primary);
}

/* The one control the drawing points at. */
.own-fake-button.is-pointed,
.own-f.is-pointed .own-f-proxy {
    outline: 2px solid var(--color-text);
    outline-offset: 3px;
}

.own-fake-results {
    margin-top: var(--space-3);
    padding: 0;
    list-style: none;
}

.own-fake-result {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    align-items: center;
    gap: var(--space-3);
    padding: 10px 2px;
    border-top: 1px solid var(--color-border);
}

.own-fake-result:first-child {
    border-top: 0;
}

.own-fake-result.is-faded {
    opacity: 0.55;
}

.own-fake-domain {
    min-width: 0;
    overflow: hidden;
    font-family: var(--font-mono);
    font-weight: var(--weight-medium);
    text-overflow: ellipsis;
    white-space: nowrap;
}

.own-fake-domain > span {
    display: block;
    margin-top: 1px;
    color: var(--color-text-muted);
    font-family: var(--font-sans);
    font-weight: var(--weight-normal);
}

/* A price, drawn as a bar: the drawing names no figure. */
.own-fake-price {
    display: block;
    width: 44px;
    height: 8px;
    border-radius: var(--radius-full);
    background: var(--color-ghost-strong);
}

/* The record form, filled in. */
.own-form {
    display: grid;
    grid-template-columns: 5.5rem 7rem minmax(0, 1fr);
    gap: var(--space-3) var(--space-2);
}

.own-f {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}

.own-f.is-proxy {
    grid-column: span 2;
}

.own-f-label {
    color: var(--color-text-muted);
    font-weight: var(--weight-medium);
    white-space: nowrap;
}

.own-f-box {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 4px;
    min-height: 30px;
    padding: 0 8px;
    overflow: hidden;
    border: 1px solid var(--color-control-border);
    border-radius: calc(var(--radius-sm) - 2px);
    background: var(--color-input);
    white-space: nowrap;
}

.own-f-box > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

.own-f-box.mono {
    font-size: 12px;
}

.own-f-box .icon {
    flex: none;
    width: 12px;
    height: 12px;
    color: var(--color-text-subtle);
}

.own-f-proxy {
    display: inline-flex;
    align-items: center;
    align-self: flex-start;
    gap: 8px;
    min-height: 30px;
    padding: 0 8px 0 4px;
    border-radius: calc(var(--radius-sm) - 2px);
    white-space: nowrap;
}

/* The switch, off: grey, the knob to the left. */
.own-switch {
    position: relative;
    flex: none;
    width: 30px;
    height: 18px;
    border-radius: var(--radius-full);
    background: var(--color-border-strong);
}

.own-switch::after {
    content: '';
    position: absolute;
    top: 2px;
    left: 2px;
    width: 14px;
    height: 14px;
    border-radius: var(--radius-full);
    background: var(--color-surface);
    box-shadow: var(--shadow-xs);
}

.own-form-foot {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2);
    margin-top: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px solid var(--color-border);
}

@media (max-width: 639px) {
    .own-form {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    }

    .own-f.is-target,
    .own-f.is-proxy {
        grid-column: span 2;
    }
}

/* The step Record: three numbered lines, the record as four rows, label
   and value stacked on a narrow screen. */
.own-steps {
    display: grid;
    gap: var(--space-4);
    padding: 0;
    list-style: none;
}

.own-steps > li {
    display: grid;
    grid-template-columns: 24px minmax(0, 1fr);
    gap: var(--space-3);
    align-items: start;
}

.own-num {
    display: grid;
    place-items: center;
    width: 24px;
    height: 24px;
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-full);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    font-variant-numeric: tabular-nums;
}

.own-step-text {
    display: grid;
    gap: var(--space-3);
    min-width: 0;
    padding-top: 1px;
    line-height: 1.5;
}

.own-step-text b {
    font-weight: var(--weight-semibold);
}

.own-record {
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-md);
    background: var(--color-surface-muted);
}

.own-record > div {
    display: grid;
    gap: 2px var(--space-4);
    padding: 10px var(--space-4);
    border-top: 1px solid var(--color-border);
}

.own-record > div:first-child {
    border-top: 0;
}

.own-record dt {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
}

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

.own-record .mono {
    font-size: 15px;
    overflow-wrap: anywhere;
}

.own-record-value {
    font-weight: var(--weight-medium);
}

.own-record-hint {
    flex-basis: 100%;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

/* A value with Copy: Copy at the end of its row, or under it, its icon
   under the value's start, where the value takes the whole row. */
.own-record [data-copy-text],
.own-alt [data-copy-text] {
    display: flex;
    flex: 1;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 2px var(--space-2);
    min-width: 0;
}

.own-record [data-copy-text] .btn,
.own-alt [data-copy-text] .btn {
    flex: none;
    min-height: 28px;
    margin: -4px -6px -4px -8px;
    padding: 0 var(--space-2);
}

.own-record .copy-status,
.own-alt .copy-status {
    flex-basis: 100%;
}

@media (min-width: 480px) {
    .own-record > div {
        grid-template-columns: 7rem minmax(0, 1fr);
        align-items: baseline;
    }

    .own-record > div:has([data-copy-text]) {
        align-items: center;
    }
}

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

/* The records of another provider. */
.own-alt {
    padding: 0;
    list-style: none;
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-md);
    background: var(--color-surface-muted);
}

.own-alt > li {
    display: grid;
    grid-template-columns: 3.25rem 1.5rem minmax(0, 1fr);
    align-items: center;
    gap: var(--space-2);
    min-height: 44px;
    padding: 4px var(--space-4);
    border-top: 1px solid var(--color-border);
}

.own-alt > li:first-child {
    border-top: 0;
}

.own-alt-type {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
}

.own-alt-name {
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

.own-alt .mono {
    min-width: 0;
    font-size: var(--text-sm);
    overflow-wrap: anywhere;
}

/* "Copy steps" could not copy: the steps, selected, to copy by hand. */
.own-steps-text {
    width: 100%;
    min-height: 13rem;
    padding: var(--space-3);
    border: 1px solid var(--color-control-border);
    border-radius: var(--radius-sm);
    background: var(--color-input);
    color: var(--color-text);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: 1.5;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    resize: vertical;
}

/* The step Check: three checks, each with its mark and state, a failed
   one with its sentence and its fix under its name. */
.own-checks {
    padding: 0;
    list-style: none;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
}

.own-check {
    display: grid;
    grid-template-columns: 22px minmax(0, 1fr) auto;
    grid-template-areas:
        "mark name state"
        ". why why";
    align-items: center;
    gap: 2px var(--space-3);
    min-height: 52px;
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--color-border);
}

.own-check:first-child {
    border-top: 0;
}

.own-mark {
    grid-area: mark;
    display: grid;
    place-items: center;
    width: 22px;
    height: 22px;
    border-radius: var(--radius-full);
}

.own-mark .icon {
    width: 13px;
    height: 13px;
    stroke-width: 3;
}

.own-mark.is-waiting {
    border: 1.5px solid var(--color-border-strong);
}

.own-mark.is-passed {
    background: var(--color-success-solid);
    color: var(--color-on-success);
}

.own-mark.is-failed {
    background: var(--color-danger-solid);
    color: var(--color-on-danger);
}

.own-check-name {
    grid-area: name;
    min-width: 0;
    font-weight: var(--weight-medium);
}

.own-check.is-waiting .own-check-name {
    color: var(--color-text-muted);
}

.own-check-state {
    grid-area: state;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

.own-check.is-passed .own-check-state {
    color: var(--color-success-text);
    font-weight: var(--weight-medium);
}

.own-check.is-failed .own-check-state {
    color: var(--color-danger-text);
    font-weight: var(--weight-medium);
}

.own-check-why {
    grid-area: why;
    display: grid;
    gap: 2px;
    font-size: var(--text-sm);
    line-height: 1.5;
}

.own-check-why .text-button,
.link-suggest .text-button,
.own-move-to .text-button {
    min-height: 24px;
    font-size: var(--text-sm);
}

/* A note under a list of a window takes the window's gap, where a field's
   note sits close under its control. */
.dialog-body > .field-note {
    margin-top: var(--space-5);
}

/* All moved: the check of D80, still, since the window played it already. */
.done.is-still .done-mark,
.done.is-still .done-ring,
.done.is-still .done-title,
.done.is-still .done-line {
    animation: none;
}

.done.is-still .done-check path {
    animation: none;
    stroke-dashoffset: 0;
}

/* The move: each link's address, and after the arrow the one it gets;
   while the domain alone is free, who gets it, a radio per row and one for
   nobody without script, a quiet button at the end of each new address
   with it (app.js). Above the card's label, which covers the whole row. */
.own-move .check-text {
    flex: 1;
}

.own-move-from,
.own-move-to {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 2px 6px;
    min-width: 0;
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    overflow-wrap: anywhere;
}

.own-move-from {
    color: var(--color-text-muted);
}

.own-move-to {
    color: var(--color-text);
}

/* The arrow stays with the address it points at. */
.own-move-new {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
}

.own-move-new > .icon {
    flex: none;
    width: 14px;
    height: 14px;
    color: var(--color-text-subtle);
}

.own-bare,
.own-move-to .text-button {
    position: relative;
    z-index: 1;
    margin-left: auto;
    font-family: var(--font-sans);
}

.own-bare {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    cursor: pointer;
}

.own-bare-none {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-top: var(--space-3);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    cursor: pointer;
}

/* The bios: per link where to paste it, with Copy and Done; a row done
   with the green check and what stopped working. */
.own-bios {
    padding: 0;
    list-style: none;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
}

.own-bio {
    display: grid;
    grid-template-columns: 26px minmax(0, 1fr);
    gap: var(--space-3);
    align-items: start;
    padding: var(--space-4);
    border-top: 1px solid var(--color-border);
}

.own-bio:first-child {
    border-top: 0;
}

.own-bio > .inv-mark,
.own-bio-mark {
    margin-top: -2px;
}

.own-bio-mark {
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    border-radius: var(--radius-full);
    background: var(--color-success-solid);
    color: var(--color-on-success);
}

.own-bio-mark .icon {
    width: 14px;
    height: 14px;
    stroke-width: 3;
}

.own-bio-main {
    display: grid;
    gap: var(--space-3);
    min-width: 0;
}

.own-bio-text {
    line-height: 1.5;
    overflow-wrap: anywhere;
}

.own-bio-text .mono {
    font-size: 15px;
}

.own-bio-line {
    margin-top: 2px;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

.own-bio-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

.own-bio-actions form {
    display: flex;
}

.own-bio-actions .copy-status {
    flex-basis: 100%;
}

/* The link window: an empty ending reads "Just the domain", in words
   rather than as an address; after the domain alone was refused, the
   handle to use instead. */
.input-join-input.mono::placeholder {
    font-family: var(--font-sans);
    font-size: var(--text-md);
}

.link-suggest {
    margin-top: -4px;
}

/* ------------------------------------------------------------------------
   Panels: a record opened from a list of records, on a wide screen, comes
   in from the right edge over the list, from the top of the screen to the
   bottom. The list behind dims but stays readable, the row, item or card
   of the open record marked (is-panel-open). app.js builds it, a modal
   <dialog>, and puts the record's own page in its body. [D76]

   The parts, left to right and top to bottom:
     .side-panel-resize  the left edge, dragged, or moved with the arrow
                         keys, to set the width (--side-panel-width), which
                         the device keeps
     .side-panel-head    the previous and the next record of the list and
                         "2 of 6" (.side-panel-step, .side-panel-count), or
                         Back a level deeper; the record's name once its
                         title has scrolled away (.side-panel-title); "Open
                         as page" and the X (.side-panel-actions). It never
                         scrolls, and has no line under it
     .side-panel-body    the record's page (.side-panel-page); the one part
                         that scrolls, and the container "panel" the page is
                         laid out by (layout.css, analytics.css)

   It slides in a little and fades, and the page behind dims; closing is
   quicker, and the next record fades in. Nothing moves for someone who
   asked for less motion.
   ------------------------------------------------------------------------ */

/* Nothing behind the panel scrolls, as behind a window. */
html:has(.side-panel[open]) {
    overflow: hidden;
}

/* By default 880 to 1040 pixels wide, as the room beside the sidebar
   allows, or beside its rail when it is folded; a width dragged replaces
   it on the element (app.js). 200 pixels of the page always stay in
   sight. */
.side-panel {
    --side-panel-beside: var(--sidebar-width);
    --side-panel-width: min(max(880px, 55vw), 1040px, calc(100vw - var(--side-panel-beside) - 96px));
    position: fixed;
    inset: 0 0 0 auto;
    width: var(--side-panel-width);
    max-width: calc(100vw - 200px);
    height: auto;
    max-height: none;
    margin: 0;
    padding: 0;
    /* Not a box that scrolls itself, as a modal dialog is by default:
       only its body scrolls. Its edge stands out over the border. */
    overflow: visible;
    border: 0;
    border-left: 1px solid var(--color-border);
    border-radius: 0;
    background: var(--color-canvas);
    color: var(--color-text);
    box-shadow: var(--shadow-panel);
}

html[data-sidebar="collapsed"] .side-panel {
    --side-panel-beside: var(--rail-width);
}

.side-panel[open] {
    display: flex;
    flex-direction: column;
}

/* The panel takes the focus as it opens; its head says where one is. */
.side-panel:focus {
    outline: none;
}

.side-panel::backdrop {
    background: var(--color-overlay-panel);
}

/* The edge to drag: nine pixels centred on the border, wider than it
   looks and above the page's sticky row of sections, with a line where
   the pointer is and while it is dragged. */
.side-panel-resize {
    position: absolute;
    top: 0;
    bottom: 0;
    left: -5px;
    z-index: 3;
    width: 9px;
    cursor: col-resize;
    touch-action: none;
}

.side-panel-resize::after {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: 4px;
    width: 2px;
    background: transparent;
    transition: background-color var(--duration-fast) var(--ease-in-out);
}

.side-panel-resize:hover::after,
.side-panel.is-resizing .side-panel-resize::after {
    background: var(--color-border-strong);
}

.side-panel-resize:focus-visible {
    outline: none;
}

.side-panel-resize:focus-visible::after {
    background: var(--color-focus);
}

/* While the edge is dragged nothing is selected and nothing eases. */
.side-panel.is-resizing,
.side-panel.is-resizing * {
    user-select: none;
    transition: none;
}

.side-panel-head {
    display: flex;
    flex: none;
    align-items: center;
    gap: var(--space-1);
    height: 56px;
    padding: 0 var(--space-3);
}

.side-panel-step,
.side-panel-actions {
    display: flex;
    gap: 2px;
}

.side-panel-count {
    margin-left: var(--space-2);
    color: var(--color-text-subtle);
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.side-panel-title {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    font-weight: var(--weight-semibold);
    text-align: center;
    text-overflow: ellipsis;
    white-space: nowrap;
    opacity: 0;
    transition: opacity var(--duration-fast) var(--ease-in-out);
}

.side-panel.is-past-title .side-panel-title {
    opacity: 1;
}

.side-panel-head .btn-icon {
    color: var(--color-text-subtle);
}

/* At an end of the list: there, but quieter than a disabled button. */
.side-panel-head .btn-icon:disabled {
    opacity: 0.35;
}

@media (hover: hover) {
    .side-panel-head .btn-icon:not(:disabled):hover {
        background-color: var(--color-surface-muted);
        color: var(--color-text);
    }
}

.side-panel-body {
    flex: 1 1 auto;
    min-height: 0;
    padding: var(--space-2) var(--space-8) var(--space-12);
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    container: panel / inline-size;
}

/* A link of a list while its record is on its way to the panel. */
a[data-panel][aria-busy="true"] {
    cursor: progress;
}

/* The record on screen, in the list behind: a row of a table or of a
   list, and a node or a row of a model's page, lie on the grey of a
   chosen row, the table's link in bold; a phone's card and a reel's cover
   get a calm ring. */
tr.is-panel-open,
li.is-panel-open > .list-item,
li.is-panel-open > .inv-node,
.inv-node.is-panel-open,
.inv-acc-rows > li.is-panel-open,
.inv-look-row.is-panel-open {
    background: var(--color-canvas-selected);
}

tr.is-panel-open a[data-panel]:not(.btn) {
    font-weight: var(--weight-semibold);
}

.inv-phone-card.is-panel-open {
    outline: 2px solid var(--color-text-muted);
    outline-offset: 2px;
}

/* Inside the cover, as the row of covers clips what stands out of it. */
li.is-panel-open > .acc-reel .rl-cover::after,
li.is-panel-open > .home-reel .rl-cover::after {
    content: '';
    position: absolute;
    inset: 0;
    border: 2px solid var(--color-text-muted);
    border-radius: inherit;
    pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
    .side-panel[open] {
        animation: side-panel-in 300ms var(--ease-out) backwards;
    }

    .side-panel[open]::backdrop {
        animation: dialog-fade-in 220ms var(--ease-in-out) backwards;
    }

    .side-panel.is-closing {
        animation: side-panel-out 170ms var(--ease-in-out) forwards;
    }

    .side-panel.is-closing::backdrop {
        animation: dialog-fade-out 170ms var(--ease-in-out) forwards;
    }
}

@keyframes side-panel-in {
    from { opacity: 0; transform: translateX(48px); }
}

@keyframes side-panel-out {
    to { opacity: 0; transform: translateX(32px); }
}

/* ------------------------------------------------------------------------
   A page on its way (D110). The server sends a page whole; what waits is
   the click before it. app.js lays .page-cover, which the layout holds
   hidden, over the content column once a link, a tab or a form asks for
   a page, and the record panel holds its own cover under its head.
   Either a cover never shows or it shows for a moment of its own: only
   once the page takes 400 ms (app.js's COVER_SHOWS), quickly faded in,
   and once it has shown it stays at least 500 ms (COVER_STAYS), so a
   page that comes just after never makes it blink in and out. It is the
   page's ground with the spinner in its middle, or, for another view of
   the same page, what it covers dimmed (.is-dim). Until it shows it is
   not there for the pointer either, so a click or a hover on the page
   that leaves goes on as before. The spinner is the buttons' ring,
   larger, its line with round ends: a track in the border's grey and an
   ink arc turning on it, both drawn as masks, so they keep one width on
   every screen.
   ------------------------------------------------------------------------ */

.wait-cover {
    /* The ring, and a quarter of it, as masks. */
    --wait-track: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 28'%3E%3Ccircle cx='14' cy='14' r='12.75' fill='none' stroke='black' stroke-width='2.5'/%3E%3C/svg%3E");
    --wait-arc: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 28'%3E%3Cpath d='M14 1.25A12.75 12.75 0 0 1 26.75 14' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E");
    display: grid;
    place-items: center;
    background: var(--color-canvas);
    cursor: progress;
}

.wait-cover.is-dim {
    background: color-mix(in srgb, var(--color-canvas) 50%, transparent);
}

.wait-cover::before,
.wait-cover::after {
    content: '';
    grid-area: 1 / 1;
    width: 28px;
    height: 28px;
}

.wait-cover::before {
    background: var(--color-border);
    -webkit-mask: var(--wait-track) center / 100% no-repeat;
    mask: var(--wait-track) center / 100% no-repeat;
}

.wait-cover::after {
    background: var(--color-text);
    -webkit-mask: var(--wait-arc) center / 100% no-repeat;
    mask: var(--wait-arc) center / 100% no-repeat;
    animation: spin 0.7s linear infinite;
}

/* The page's: over the content column, above the phone's small title and
   under its search and its bar, beside the sidebar or its rail. On a
   phone the spinner sits in the middle of what the bar leaves. */
.page-cover {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: var(--z-page-cover);
    display: none;
}

@media (max-width: 639px) {
    .page-cover {
        padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 72px);
    }
}

@media (min-width: 640px) {
    .page-cover {
        left: var(--rail-width);
    }
}

@media (min-width: 1024px) {
    .page-cover {
        left: var(--sidebar-width);
    }

    html[data-sidebar="collapsed"] .page-cover {
        left: var(--rail-width);
    }
}

/* Laid as the page is asked for (app.js), shown once it takes 400 ms,
   quickly faded in. One laid while a cover taken over from the page
   before still showed goes on at once (is-held). */
.page-cover.is-waiting {
    display: grid;
    animation: wait-cover-in 150ms var(--ease-out) 400ms backwards;
}

.page-cover.is-waiting.is-held {
    animation: none;
}

/* Taken over by the page that came while it showed (wait.js): whole from
   its first paint, so nothing of the page shows before it, its arc where
   it had turned, held for what is left of its 500 ms, then fading away;
   for another view, under the filter row. */
html.is-arriving .page-cover {
    display: grid;
    animation: wait-cover-out 200ms var(--ease-in-out) var(--arriving-hold, 0ms) forwards;
}

html.is-arriving .page-cover::after {
    animation-delay: var(--arriving-turn, 0ms);
}

html.is-arriving-dim .page-cover {
    top: var(--arriving-top, 0px);
    background: color-mix(in srgb, var(--color-canvas) 50%, transparent);
}

/* The record panel's: over its body, under its head, while a record, or
   the page the panel leaves for, is on its way (app.js marks the body
   aria-busy, with data-wait): another record and a level deeper cover,
   the record in another view or after a change dims what lies under its
   filter row (app.js sets the top), and the panel's first record, whose
   panel opened only once it took 400 ms, shows its spinner at once. Once
   shown it stays its 500 ms before the record's page shows (app.js holds
   it back), then goes, quickly fading; the panel leaving for a page goes
   with it. */
.side-panel-cover {
    position: absolute;
    top: 56px;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 2;
    visibility: hidden;
    opacity: 0;
    transition: opacity var(--duration-fast) var(--ease-out), visibility 0s linear var(--duration-fast);
}

.side-panel:has(> .side-panel-body[aria-busy="true"]) > .side-panel-cover,
.side-panel.is-leaving > .side-panel-cover {
    visibility: visible;
    opacity: 1;
    transition: opacity 150ms var(--ease-out) 400ms, visibility 0s linear 400ms;
}

.side-panel:has(> .side-panel-body[data-wait="dim"]) > .side-panel-cover {
    background: color-mix(in srgb, var(--color-canvas) 50%, transparent);
}

.side-panel:has(> .side-panel-body[data-wait="open"]) > .side-panel-cover {
    transition-delay: 0s;
}

/* The arc turns only while it is seen. */
.side-panel-cover::after {
    animation-play-state: paused;
}

.side-panel:has(> .side-panel-body[aria-busy="true"]) > .side-panel-cover::after,
.side-panel.is-leaving > .side-panel-cover::after {
    animation-play-state: running;
}

@keyframes wait-cover-in {
    from { visibility: hidden; opacity: 0; }
}

@keyframes wait-cover-out {
    to { visibility: hidden; opacity: 0; }
}

/* For less motion: a cover comes and goes without fading, and the arc
   turns slower. */
@media (prefers-reduced-motion: reduce) {
    .page-cover.is-waiting {
        animation-duration: 1ms;
    }

    html.is-arriving .page-cover {
        animation-duration: 1ms;
    }

    .side-panel-cover,
    .side-panel:has(> .side-panel-body[aria-busy="true"]) > .side-panel-cover,
    .side-panel.is-leaving > .side-panel-cover {
        transition-duration: 0s;
    }

    .wait-cover::after {
        animation-duration: 1.4s;
    }
}

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

.error-page {
    max-width: 34rem;
    margin: var(--space-10) auto;
    text-align: center;
}

.error-page-icon {
    display: grid;
    place-items: center;
    width: 48px;
    height: 48px;
    margin: 0 auto var(--space-5);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface-muted);
    color: var(--color-text-muted);
}

.error-page-status {
    color: var(--color-text-subtle);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

.error-page-title {
    margin-top: var(--space-2);
    font-size: clamp(var(--text-2xl), 1.2rem + 1vw, var(--text-3xl));
    letter-spacing: -0.025em;
}

.error-page-text {
    margin-top: var(--space-3);
    color: var(--color-text-muted);
    font-size: var(--text-md);
}

.error-page .alert {
    margin-top: var(--space-5);
    text-align: left;
}

.error-page-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-3);
    margin-top: var(--space-6);
}

.error-page-ref {
    margin-top: var(--space-8);
    color: var(--color-text-subtle);
    font-size: var(--text-xs);
}

.error-page-ref code {
    color: var(--color-text-muted);
    overflow-wrap: anywhere;
}

/* In the sign in shell the error sits in the form column and reads like the
   sign in form: left aligned. */
.auth-col .error-page {
    margin: 0;
    text-align: left;
}

.auth-col .error-page-icon {
    margin-left: 0;
}

.auth-col .error-page-actions {
    justify-content: flex-start;
}

@media (max-width: 479px) {
    .error-page-actions {
        flex-direction: column;
    }
}

/* ------------------------------------------------------------------------
   Code blocks: text whose lines matter, such as the full error of a failed
   job run with its trace. Long lines wrap inside the block rather than
   widening the page, which on a phone would scroll sideways. No ligatures:
   JetBrains Mono would draw the -> of a trace as an arrow.
   ------------------------------------------------------------------------ */

.code-block {
    margin: 0;
    padding: var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface-muted);
    color: var(--color-text);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-variant-ligatures: none;
    line-height: var(--leading-normal);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    tab-size: 4;
}

/* A picture of a model or a team member in place of a model's portrait or
   a team member's initials, which keeps the avatar's size, circle and
   border: the square fills the circle (partials/picture.php). [Phase 4,
   section 8.15] */
img.is-picture {
    display: block;
    object-fit: cover;
    background-color: var(--color-surface-muted);
}

/* A model's drawn portrait where it has no picture (partials/picture.php,
   Pictures\Portraits): a figure in a lit room, its head and shoulders soft
   gradients under a small blur, never an image. Any round avatar is its
   frame (.is-portrait) and clips the drawing, which reaches past the edge
   so that the blur leaves no pale rim. The avatar is a container, so the
   drawing is measured in its size and the blur in cqi: the same picture
   at 30 pixels as at 72, blurred about 1.5 pixels at 36. Each look,
   .portrait-1 to .portrait-10, takes its colours from tokens.css and says
   where its light falls; look 1, sunset, is the first model's. */
.is-portrait {
    position: relative;
    overflow: hidden;
    container-type: inline-size;
}

.portrait {
    position: absolute;
    inset: -11%;
    background:
        radial-gradient(ellipse 26% 22% at 50% 40%, var(--portrait-skin) 0 55%, transparent 74%),
        radial-gradient(ellipse 52% 40% at 50% 105%, var(--portrait-wear) 0 60%, transparent 76%),
        radial-gradient(circle at var(--portrait-light-at), var(--portrait-light) 0, transparent 50%),
        linear-gradient(175deg, var(--portrait-top), var(--portrait-bottom));
    filter: blur(4.2cqi) saturate(0.92);
}

.portrait-1 {
    --portrait-top: var(--portrait-sunset-top);
    --portrait-bottom: var(--portrait-sunset-bottom);
    --portrait-light: var(--portrait-sunset-light);
    --portrait-light-at: 50% 30%;
    --portrait-skin: var(--portrait-sunset-skin);
    --portrait-wear: var(--portrait-sunset-wear);
}

.portrait-2 {
    --portrait-top: var(--portrait-beach-top);
    --portrait-bottom: var(--portrait-beach-bottom);
    --portrait-light: var(--portrait-beach-light);
    --portrait-light-at: 78% 8%;
    --portrait-skin: var(--portrait-beach-skin);
    --portrait-wear: var(--portrait-beach-wear);
}

.portrait-3 {
    --portrait-top: var(--portrait-room-top);
    --portrait-bottom: var(--portrait-room-bottom);
    --portrait-light: var(--portrait-room-light);
    --portrait-light-at: 20% 18%;
    --portrait-skin: var(--portrait-room-skin);
    --portrait-wear: var(--portrait-room-wear);
}

.portrait-4 {
    --portrait-top: var(--portrait-studio-top);
    --portrait-bottom: var(--portrait-studio-bottom);
    --portrait-light: var(--portrait-studio-light);
    --portrait-light-at: 40% 12%;
    --portrait-skin: var(--portrait-studio-skin);
    --portrait-wear: var(--portrait-studio-wear);
}

.portrait-5 {
    --portrait-top: var(--portrait-night-top);
    --portrait-bottom: var(--portrait-night-bottom);
    --portrait-light: var(--portrait-night-light);
    --portrait-light-at: 85% 20%;
    --portrait-skin: var(--portrait-night-skin);
    --portrait-wear: var(--portrait-night-wear);
}

.portrait-6 {
    --portrait-top: var(--portrait-mirror-top);
    --portrait-bottom: var(--portrait-mirror-bottom);
    --portrait-light: var(--portrait-mirror-light);
    --portrait-light-at: 30% 10%;
    --portrait-skin: var(--portrait-mirror-skin);
    --portrait-wear: var(--portrait-mirror-wear);
}

.portrait-7 {
    --portrait-top: var(--portrait-gym-top);
    --portrait-bottom: var(--portrait-gym-bottom);
    --portrait-light: var(--portrait-gym-light);
    --portrait-light-at: 15% 6%;
    --portrait-skin: var(--portrait-gym-skin);
    --portrait-wear: var(--portrait-gym-wear);
}

.portrait-8 {
    --portrait-top: var(--portrait-pool-top);
    --portrait-bottom: var(--portrait-pool-bottom);
    --portrait-light: var(--portrait-pool-light);
    --portrait-light-at: 70% 14%;
    --portrait-skin: var(--portrait-pool-skin);
    --portrait-wear: var(--portrait-pool-wear);
}

.portrait-9 {
    --portrait-top: var(--portrait-car-top);
    --portrait-bottom: var(--portrait-car-bottom);
    --portrait-light: var(--portrait-car-light);
    --portrait-light-at: 90% 40%;
    --portrait-skin: var(--portrait-car-skin);
    --portrait-wear: var(--portrait-car-wear);
}

.portrait-10 {
    --portrait-top: var(--portrait-street-top);
    --portrait-bottom: var(--portrait-street-bottom);
    --portrait-light: var(--portrait-street-light);
    --portrait-light-at: 60% 4%;
    --portrait-skin: var(--portrait-street-skin);
    --portrait-wear: var(--portrait-street-wear);
}
