/*
 * Form fields.
 *
 * A form takes a size the screen deserves: a name and an amount are not stretched across the
 * full width. On the desktop the working format is half the container — the form on the left,
 * the comments and explanations on the right (core.md §7.1).
 */

.app-form {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    /* Half the container, capped so it does not grow with the monitor (core.md §4.15). */
    max-width: var(--w-form);
}

.app-form-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: start;
    gap: var(--space-6);
}

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

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

/*
 * Two short fields sharing one line — an amount and a date, a code and a name.
 *
 * Each of them alone needs a third of the form's width, and a whole row for either is empty
 * space; in a modal that empty space is what makes the form scroll. Equal halves rather than content-sized columns: two forms with different pairs
 * still put their controls on the same vertical lines (core.md §7.8).
 *
 * `minmax(0, 1fr)` and not `1fr`: a control's intrinsic width must not widen its half, or the
 * halves stop being halves.
 */
.app-form__pair {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-2);
    align-items: start;
}

/* Three short fields in one row: the number and the two dates of a document (the form column is 420px). */
.app-form__pair--triple {
    grid-template-columns: repeat(3, minmax(0, 1fr));
}

/* On a folded phone half a row is not enough for anything: the pair folds back into a column. */
@media (max-width: 480px) {
    .app-form__pair {
        grid-template-columns: minmax(0, 1fr);
    }
}

/*
 * A field whose content is a date or a short code, standing alone in its row.
 *
 * Where such a field has no partner to pair with, it is capped instead of stretched: a date
 * across the whole window promises a sentence and holds ten characters.
 */
.app-field--short {
    max-width: var(--w-field-short);
}

/* Labels are sentence case; no caps anywhere, in labels or in state badges. */
.app-field__label {
    color: var(--text-3);
    font-size: var(--fs-caption);
}

/*
 * A value shown in a field's place, not asked for (the PRO editor's From, Bill to and Bank, map А.Б2): read as
 * text, an address keeps its line breaks, a long word wraps instead of widening the card.
 */
.app-field__shown {
    white-space: pre-line;
    overflow-wrap: anywhere;
}

/*
 * A value shown with its copy button (a bank card read by the accountant): the text and a fixed slot for the glyph,
 * so the glyphs of every value stand in one column whatever the text wraps to (core.md §4.4).
 */
.app-field__copy {
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--icon-btn);
    align-items: start;
    gap: var(--space-2);
}

/*
 * A field that has to be filled in says so before it is skipped, not after.
 *
 * Red means «required», not «invalid»: a refusal is orange (D-086, owner A76) and red is kept for
 * what destroys. Small: the mark points at a label, it is not a label of its own. Hidden from
 * screen readers — the control is already marked required for them.
 */
.app-field__required {
    margin-left: var(--inset-mark);
    color: var(--negative);
}

/*
 * The same mark on a field whose caption went into the placeholder.
 *
 * There is no label to stand beside, so it takes the top corner of the control: out of the way
 * of anything typed, out of the way of the placeholder, and still the first mark the eye finds
 * on a field that has to be filled in.
 */
.app-field--marked {
    position: relative;
}

.app-field--marked > .app-field__required {
    position: absolute;
    top: 4px;
    right: 8px;
    margin: 0;
    font-size: var(--fs-caption);
    line-height: 1;
    pointer-events: none;
}

/*
 * A date control keeps its calendar button in that same corner, so the mark steps aside for
 * it: two glyphs stacked in one spot would hide each other exactly when the empty required
 * field is asking to be noticed.
 */
.app-field--marked:has(> input[type='date']) > .app-field__required {
    right: 36px;
}

/*
 * A refusal that belongs to no field of the form.
 *
 * Above the fields rather than below them: it is the reason the form came back at all, and the
 * owner reads it before looking for what to change.
 *
 * Orange, not red (D-086, owner A76, defaults 3.10.11): a rule refused the action — an invalid
 * form, a payment that did not go through, a balance that blocks — and asks the owner to act;
 * red stays for what destroys. The same orange as the invalid control below.
 */
.app-form__error {
    padding: var(--inset-row) var(--space-2);
    border: 1px solid var(--warn);
    border-radius: var(--radius-control);
    color: var(--warn);
    font-size: var(--fs-caption);
}

.app-field__control {
    height: var(--field);
    padding: 0 var(--inset-control);
    border: 1px solid var(--border-control);
    border-radius: var(--radius-control);
    background: var(--surface);
    font-size: var(--fs-body);
    transition: border-color var(--transition);
}

.app-field__control:hover {
    border-color: var(--border-3);
}

/* The ring of a button (base.css `:focus-visible`): a field is where the caret is, and the edge
   colour alone was too faint to find it. */
.app-field__control:focus {
    border-color: var(--accent);
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Inert: the same fill as a disabled button, no hover, no pointer. */
.app-field__control:disabled {
    border-color: var(--border);
    background: var(--surface-2);
    color: var(--text-disabled);
    cursor: not-allowed;
}

/*
 * Read-only (the PRO read-only window, map В.Б1): the value stays legible and can be selected and copied, so the
 * text keeps its colour; the fill of an inert control says it is not for typing.
 */
.app-field__control[readonly] {
    border-color: var(--border);
    background: var(--surface-2);
}

/*
 * A date: the text field with the day written as the owner reads it (D-026, «23 Oct 2026») and the
 * calendar glyph button inside its right edge, as on the old site (KINV-124). The button is the
 * ordinary quiet icon button, set into the 44px field with the 2px the border and the ring leave.
 */
.app-field__date {
    position: relative;
    display: flex;
    flex-direction: column;
}

.app-field__date > .app-field__control {
    padding-right: var(--field);
}

.app-field__date > .app-icon-btn {
    position: absolute;
    top: 2px;
    right: 2px;
}

/* Amounts and any other figures are tabular and never wrap, in the input as well. */
.app-field__control--num {
    font-variant-numeric: var(--numeric);
    text-align: right;
}

textarea.app-field__control {
    height: auto;
    min-height: var(--field-multiline);
    padding: var(--inset-row) var(--inset-control);
    line-height: var(--lh-text);
    resize: vertical;
}

/*
 * An invalid control: soft orange fill and an orange border (owner A76, defaults 3.10.11; variant 2
 * of ui/kit/invalid.html). Stated after `:hover` and `:focus` so the border keeps its colour under
 * them, while the focus outline above stays and still shows where the caret is. `:not(:disabled)`: an
 * inert control keeps the disabled grey even if it is marked invalid. A document line is
 * not wrapped in `.app-field`, so its control carries the modifier itself (`aria-invalid` is set too).
 */
.app-field--invalid .app-field__control:not(:disabled),
.app-lines .app-lines__control--invalid:not(:disabled) {
    border-color: var(--warn);
    background: var(--scale-warn-1);
}

/* The caption sits under the control, on the surface, not on the tint: --warn reads 4.95:1 there. */
.app-field__error {
    color: var(--warn);
    font-size: var(--fs-caption);
}

/* --- Choice tiles -------------------------------------------------------------------- */

/*
 * A choice among few options is always tiles, not a select. «Few» means as many as fit as
 * tiles, usually in one row — the threshold is `app.ui.tilesMaxOptions`. The select survives
 * where seeing the whole list matters (app\ui\FormField::choice).
 */
.app-tiles {
    /*
     * Tiles of one function are one width (owner, 23.10.2026): a grid of equal columns, so the
     * widest label sets the width of every tile, instead of pills as wide as their words.
     */
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    gap: var(--space-1);
}

/* A phone: the row would run off the screen, so the equal cells wrap into rows of equal cells. */
@media (max-width: 480px) {
    .app-tiles {
        grid-auto-flow: row;
        grid-auto-columns: auto;
        grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    }
}

.app-tile {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-1);
    min-height: var(--control);
    padding: 0 var(--inset-tile);
    /* `--tile-edge`: a group of tiles may carry a lighter resting edge (the layout, item-type and
       language tiles, owner 23.10.2026); hover, picked and inert states set their own. */
    border: 1px solid var(--tile-edge, var(--border-control));
    border-radius: var(--radius-tile);
    background: var(--surface);
    color: var(--text-2);
    cursor: pointer;
    transition: background var(--transition), border-color var(--transition), color var(--transition);
}

.app-tile:has(input:focus-visible) {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.app-tile:hover {
    background: var(--surface-2);
    border-color: var(--border-3);
}

/*
 * The pressed state holds on three signals at once: soft fill, accent text, weight 600.
 *
 * :has, because the control is a real radio input hidden inside the label — the choice then
 * needs no script at all and survives a form that comes back with errors.
 */
.app-tile:has(input:checked),
.app-tile[aria-pressed='true'],
.app-tile--on {
    border-color: var(--accent-border);
    background: var(--accent-soft-2);
    color: var(--accent);
    font-weight: var(--fw-semibold);
}

/*
 * The steps of a purchase (З.Б15): tiles that are read, not picked, with an arrow between each two.
 *
 * Solid accent tiles and accent arrows: the soft pressed fill cannot give an arrow "of the same colour" that is seen
 * (owner, 24.10.2026). A flex row, not the equal-column grid: the arrows are items of the list, so they must not
 * become grid cells of tile width; the tiles share what the arrows leave. Under --bp-unfolded the row stacks and
 * the arrows turn down. The 20px glyph is drawn at 32px with a 2.5 stroke, about 4px on the screen.
 */
.app-tiles--steps {
    --steps-arrow: 32px;

    display: flex;
    align-items: stretch;
    gap: var(--space-1);
    /* The row sits between the lead line and the note, both plain paragraphs without a margin of their own. */
    margin-block: var(--space-2);
}

.app-tiles--steps .app-tile {
    flex: 1 1 0;
    min-width: 0;
    padding: var(--space-1);
    text-align: center;
    cursor: default;
}

.app-tiles--steps .app-tile--on,
.app-tiles--steps .app-tile--on:hover {
    border-color: var(--accent);
    background: var(--accent);
    color: var(--surface);
}

.app-tiles--steps .app-icon {
    width: var(--steps-arrow);
    height: var(--steps-arrow);
    color: var(--accent);
    stroke-width: 2.5;
}

.app-tiles--steps > li[aria-hidden] {
    display: flex;
    flex: none;
    align-items: center;
}

.app-tiles--steps .app-tiles__down {
    display: none;
}

@media (max-width: 720px) {
    .app-tiles--steps {
        flex-direction: column;
        align-items: stretch;
    }

    .app-tiles--steps .app-tile {
        flex: none;
    }

    .app-tiles--steps > li[aria-hidden] {
        justify-content: center;
    }

    .app-tiles--steps .app-tiles__right {
        display: none;
    }

    .app-tiles--steps .app-tiles__down {
        display: block;
    }
}

/*
 * A tile with nothing to answer about yet.
 *
 * `:has(input:disabled)`, not `:disabled`: the tile is a label, and a label is never
 * disabled itself — the radio hidden inside it is. The hover is taken back for the same
 * reason it is given, and the cursor says the tile is not waiting for a click.
 */
.app-tile:has(input:disabled) {
    border-color: var(--border);
    background: var(--surface-2);
    color: var(--text-disabled);
    cursor: not-allowed;
}

.app-tile:has(input:disabled):hover {
    background: var(--surface-2);
    border-color: var(--border);
}

/*
 * A tile that carries a picture: the layout chooser (the old site's theme tiles).
 *
 * The picture comes first and the name under it, one tile — one function — one size, so the
 * grid of `.app-tiles` sets the width and the picture fills it. A tile the user may not pick
 * (a PRO layout for a guest) is the disabled tile of above; its picture is dimmed as well.
 */
.app-tile--pic {
    flex-direction: column;
    justify-content: flex-start;
    gap: var(--space-0);
    padding: var(--space-1);
    font-size: var(--fs-caption);
    text-align: center;
}

.app-tile__pic {
    display: block;
    width: 100%;
    height: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius-glyph);
    background: var(--surface);
}

.app-tile--pic:has(input:disabled) .app-tile__pic {
    opacity: var(--opacity-muted);
}

/* --- Form sections ------------------------------------------------------------------- */

/*
 * The sections of ONE form, switched in place: the document editor's two strips, Document ·
 * From · Bill to · Items and Design · Totals · Bank · More (the old constructor's tabs, KINV-121).
 * They are not the route tabs of tabs.css, which are links to pages; here nothing is loaded and
 * every field stays in the form, so a hidden section is still submitted and still counted.
 *
 * Each strip is a group of its own (`.app-form-group`) and switches only its own panels, so one
 * panel of each strip is open at once. A section that holds a field with an error carries a
 * mark, so the error is not hidden behind another tab.
 */
.app-form-tabs {
    display: flex;
    gap: var(--space-0);
    border-bottom: 1px solid var(--border);
}

.app-form-tabs__tab {
    /* As wide as its word, standing at the left, as the old strip drew them. */
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-0);
    min-height: var(--field);
    padding: var(--space-1) var(--space-2);
    border: 0;
    /* The open tab is a line that continues the border of the strip, as in tabs.css. */
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    background: none;
    color: var(--text-3);
    font: inherit;
    font-size: var(--fs-caption);
    font-weight: var(--fw-semibold);
    cursor: pointer;
    transition: color var(--transition), border-color var(--transition);
}

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

/* The open tab is dark text on the accent line, as the old strip had it. */
.app-form-tabs__tab[aria-selected='true'] {
    border-bottom-color: var(--accent);
    color: var(--text);
}

/*
 * One strip with its panels. The panels keep a minimum height so that switching a tab does not
 * move what stands below the group (the old site reserved 20rem and 26rem); a taller panel grows.
 */
.app-form-group {
    min-width: 0;
}

.app-form-group__panels {
    min-height: 20rem;
}

.app-form-group--settings .app-form-group__panels {
    min-height: 26rem;
}

.app-form-tabs__mark {
    /* The same «not accepted» meaning as the invalid control (A76), so the same orange. */
    color: var(--warn);
    font-weight: var(--fw-bold);
}

.app-form-panel {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding-top: var(--space-3);
}

@media (pointer: coarse) {
    .app-form-tabs__tab {
        min-height: var(--control-touch);
    }
}

/* --- Touch --------------------------------------------------------------------------- */

@media (pointer: coarse) {
    .app-field__control {
        height: var(--field-touch);
        border-radius: var(--radius);
    }

    .app-tile {
        min-height: var(--control-touch);
        border-radius: var(--radius-tile);
    }
}

/* On a folded phone the action panel sticks to the bottom and the button spans the width. */
@media (max-width: 480px) {
    .app-form {
        max-width: none;
    }

    .app-form__actions {
        position: sticky;
        bottom: 0;
        display: flex;
        gap: var(--space-2);
        height: 56px;
        margin: 0 calc(-1 * var(--space-3));
        padding: var(--space-1) var(--space-3);
        border-top: 1px solid var(--border);
        background: var(--surface);
    }

    .app-form__actions .app-btn {
        flex: 1;
    }
}

/* --- Filters ------------------------------------------------------------------------- */

/*
 * A quick search stands in one row above the list it narrows: three short fields and the two
 * buttons that run and clear them. It is not a form in the sense the dialog forms are —
 * nothing is saved by it — so it is not held to the half-width rule; it is as wide as the
 * table it belongs to and folds into one column on a narrow screen.
 *
 * Every field is exactly two lines tall — a label and a control, never a hint. Fields of
 * different heights aligned by their bottoms is what made the controls sit at four different
 * levels; the row read as scattered rather than as a row (core.md §6.6: one panel, one height).
 *
 * They share the width equally, and a field holding words rather than a number gets half again
 * as much of it.
 */
.app-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-3);
    /* The row stands directly over the list it narrows: they are one thing, not two blocks. */
    padding-bottom: var(--space-2);
}

/* Every field takes an equal share of the row and none of them shrinks below a readable width. */
.app-filters .app-field {
    flex: 1 1 160px;
}

/* A field that holds words rather than a number or a code is given half again as much room. */
.app-filters .app-field--wide {
    flex-grow: 1.5;
}

/* A search alone in its row is as wide as a form, not as the table: a long line is not typed into it. */
.app-filters .app-field--search {
    flex: 0 1 var(--w-form);
}

/*
 * A search that shares its row with tile groups is as wide as an email needs, not as a form: at a form's
 * width the tiles no longer fit beside it and the row broke into two lines (KINV-255, owner 30.10.2026).
 */
.app-filters:has(.app-tiles) .app-field--search {
    flex: 1 1 var(--w-field-short);
    max-width: var(--w-form);
}

/* On the wide filter page (Payments) the search keeps its short width so the whole row stays on one line. */
.app-filters--wide:has(.app-tiles) .app-field--search {
    flex: 0 1 var(--w-field-short);
}

/*
 * A tile group of a filter row is one segmented control and is as wide as its words: seven equal columns of the
 * widest label («Accountant») took 810px of a 1068px row and pushed the search to a line of its own. The equal
 * columns stay on a form, where each tile is a choice of its own, and on a phone (below).
 */
@media (min-width: 481px) {
    .app-filters .app-tiles {
        grid-auto-columns: auto;
    }
}

/* A date is as wide as a date and no wider: the control draws its own format. */
.app-filters .app-field--date {
    flex: 0 0 auto;
}

/*
 * A line break inside the row.
 *
 * A filter row is one group, and a group that wraps wherever the width happens to run out
 * breaks in the middle of a thought. This says where it breaks: everything after it starts a
 * new line, and the fields that belong together stay together (core.md §6.6).
 */
.app-filters__break {
    flex-basis: 100%;
    height: 0;
}

/* A wrapper that carries a screen address for its tile group and takes no box of its own. */
.app-filters__item {
    display: contents;
}

/* A row of tiles inside a filter takes only what its tiles need. */
.app-filters .app-field:has(.app-tiles) {
    flex: 0 1 auto;
}

/*
 * Tiles in a filter row are one control, not four buttons that happen to stand together.
 *
 * On a form each tile is a choice of its own and looks like one. In a filter the group answers
 * a single question, and separate outlines said four questions instead. One frame around them
 * says it is one, and it is exactly as tall as the fields beside it (core.md §6.4).
 */
.app-filters .app-tiles {
    height: var(--field);
    gap: 2px;
    padding: var(--inset-frame);
    border: 1px solid var(--border-control);
    border-radius: var(--radius-tile);
    background: var(--surface);
}

.app-filters .app-tile {
    min-height: 0;
    height: 100%;
    padding: 0 var(--inset-control);
    border: 0;
    border-radius: calc(var(--radius-tile) - var(--inset-frame));
    background: transparent;
}

.app-filters .app-tile:hover {
    background: var(--surface-2);
}

.app-filters .app-tile:has(input:checked) {
    background: var(--accent-soft-2);
}

/*
 * On touch a tile is a finger's target (core.md §19.1: at least --control-touch tall): the frame grows around its
 * tiles by its inset and edge instead of squeezing them into the field's height.
 */
@media (pointer: coarse) {
    .app-filters .app-tiles {
        height: auto;
    }

    .app-filters .app-tile {
        min-height: var(--control-touch);
    }
}

/* The filter group is one fixed-height frame: it keeps its single row on a phone as well. */
@media (max-width: 480px) {
    .app-filters .app-tiles {
        grid-auto-flow: column;
        grid-auto-columns: 1fr;
        grid-template-columns: none;
    }
}

/*
 * More tiles than a phone is wide (Ж.Б2 has seven): the one frame keeps its single row and scrolls inside itself, so
 * the page never scrolls sideways (core.md §5.5). A group that fits is unchanged.
 */
@media (max-width: 480px) {
    .app-filters .app-field:has(.app-tiles) {
        min-width: 0;
        max-width: 100%;
    }

    /* Relative: the off-screen radios (`.app-sr`) are clipped by this box instead of widening the page. */
    .app-filters .app-tiles {
        position: relative;
        min-width: 0;
        overflow-x: auto;
    }

    /* A label keeps its one line: the frame scrolls, a tile does not break «Read-only» in two. */
    .app-filters .app-tile {
        white-space: nowrap;
    }
}

/*
 * A line of explanation inside a form.
 *
 * Rare on purpose: a caption, a control and a line of explanation are three lines for one field,
 * and core.md §7.2 sends what can be said in a placeholder or a tooltip there instead. What is
 * left for this is what belongs to the whole form rather than to any one field — what pressing
 * the button will do, and whether it can be undone.
 */
.app-form__note {
    margin: 0;
    color: var(--text-3);
    font-size: var(--fs-caption);
}

/*
 * A note that flags a state the form is about to disturb rather than merely explain (rules.md
 * §9.5; orange is the colour of attention throughout the project — badge.css).
 */
.app-form__note--warn {
    color: var(--warn);
}

/*
 * A figure shown on a form as a fact rather than asked for.
 *
 * Where a rule refuses unless a figure is zero, the figure is how the owner learns what to move
 * before trying again.
 */
.app-form__figures {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
}

/*
 * A named group inside a filter row reads along the row, not down it.
 *
 * On a form the caption stands above its control, because the eye goes down a column of fields.
 * A filter row goes across, and three captions stacked over three groups turn one line into two
 * for no gain: a caption and its tiles are a phrase, and a phrase reads left to right.
 */
.app-filters .app-field:has(.app-tiles) {
    flex-direction: row;
    align-items: center;
    gap: var(--space-2);
}

.app-filters .app-field:has(.app-tiles) .app-field__label {
    white-space: nowrap;
}

/* --- The editor's own controls (KINV-121: the old constructor's form) ----------------- */

/*
 * Tiles as wide as the widest label and no wider: the document type and the language switch do
 * not stretch across a 530px card. Still one function — one size: an inline grid keeps the equal
 * columns, only the row is as short as its content. The item type is not one of them: its tiles
 * fill the row (`--row`).
 */
/*
 * FormField::tiles() draws the plain `.app-tiles`; the editor names the kind on the field box instead
 * (`fieldClass` app-field--fit / --glyph / --row, KINV-60), so each kind is stated for both spellings.
 */
.app-tiles--fit,
.app-field--fit > .app-tiles {
    display: inline-grid;
    align-self: flex-start;
}

/*
 * Currency by its code (never a symbol, defaults 3.10.14): one function, one size (D-072 R4) — equal
 * fixed columns, wide enough for the widest code, wrapping into rows; a flex row sized every tile by
 * its own letters.
 */
.app-tiles--glyph,
.app-field--glyph > .app-tiles {
    display: grid;
    grid-auto-flow: row;
    grid-auto-columns: auto;
    grid-template-columns: repeat(auto-fill, calc(var(--field) * 1.5));
}

.app-tiles--glyph .app-tile,
.app-field--glyph > .app-tiles .app-tile {
    padding: 0 var(--space-1);
    font-size: var(--fs-card);
    font-weight: var(--fw-semibold);
}

/*
 * The layout tiles span the whole row, one equal column per tile (owner, 23.10.2026: not a
 * handful of 72px chips with a gap at the right). On a phone the same equal cells wrap into rows;
 * both rules are stated here so they win over the plain `.app-tiles` ones above.
 */
.app-tiles--pics {
    --tile-edge: var(--border);
    display: grid;
    grid-auto-flow: column;
    /* minmax(0, …): a long layout name never pushes the row wider than its card. */
    grid-auto-columns: minmax(0, 1fr);
    grid-template-columns: none;
}

/*
 * Where one row would cut the names: «Corporate» needs a tile of 73 px (71 px of words at --fs-caption,
 * the border), eight such tiles and seven --space-1 gaps 640 px. That is the width of the tiles' own box,
 * a container size, not a screen breakpoint: the preview is about 510 px wide at 1081 px and 651 px at
 * 1440 px, so the eight tiles stand in two rows of four only in the narrow desktop range (KINV-122 §6,
 * KINV-60). The phone rule below comes after it and wins on a phone.
 */
.app-field:has(> .app-tiles--pics) {
    container-type: inline-size;
}

@container (max-width: 639px) {
    .app-tiles--pics {
        grid-auto-flow: row;
        grid-auto-columns: auto;
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }
}

/* A phone (--bp-folded): the cells of at least 72 px wrap as many to a row as fit. */
@media (max-width: 480px) {
    .app-tiles--pics {
        grid-auto-flow: row;
        grid-auto-columns: auto;
        grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
    }
}

/* The tiles of one choice that fill their row: the item type, two or three equal columns. */
.app-tiles--row,
.app-field--row > .app-tiles {
    --tile-edge: var(--border);
}

/*
 * The editor's picked tile is dark and solid, as on the old site (owner A74, 23.10.2026): the
 * document type, the language, the currency and the item type. A departure from core.md §17.7
 * (soft fill) — product-kit.md §7. The layout tiles (`--pics`) keep their picture and the frame
 * of the plain picked tile. Stated over `.app-tile:hover` so a hovered picked tile stays dark.
 */
.app-editor :is(.app-tiles--fit, .app-tiles--glyph, .app-tiles--row) .app-tile:has(input:checked),
.app-editor :is(.app-tiles--fit, .app-tiles--glyph, .app-tiles--row) .app-tile:has(input:checked):hover,
.app-editor :is(.app-field--fit, .app-field--glyph, .app-field--row) > .app-tiles .app-tile:has(input:checked),
.app-editor :is(.app-field--fit, .app-field--glyph, .app-field--row) > .app-tiles .app-tile:has(input:checked):hover {
    border-color: var(--accent);
    background: var(--accent);
    color: var(--surface);
}

/*
 * A layout the guest may not pick: the old site marked it by a small orange corner and kept the
 * name legible — the tile is a locked tile, not a dead one, so the disabled grey of above is
 * taken back for it and only the picture is dimmed (field.css `.app-tile--pic`).
 */
.app-tile--locked:has(input:disabled),
.app-tile--locked:has(input:disabled):hover {
    border-color: var(--border);
    background: var(--surface);
    color: var(--text-3);
    cursor: pointer;
}

/* The corner takes the radius of the tile, so the tile clips it. */
.app-tile--pic.app-pro-corner {
    overflow: hidden;
}

/* --- Colour swatches ------------------------------------------------------------------ */

/*
 * Curated muted tones instead of a free picker (the old site's owner decision #43): a swatch is
 * a square radio, the picked one carries a white check and a dark frame.
 *
 * The accents are the old site's exact print-safe values (`--swatch-accent-1…8`, KINV-124); the
 * text colours run from black to grey (`--swatch-text-1…6`, KINV-153) — tokens of colors.css.
 */
.app-swatches {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
}

.app-swatch {
    position: relative;
    display: block;
    width: var(--field);
    height: var(--field);
    border: 2px solid var(--border);
    border-radius: var(--radius-glyph);
    background: var(--swatch);
    cursor: pointer;
    transition: border-color var(--transition);
}

.app-swatch:hover {
    border-color: var(--border-3);
}

.app-swatch:has(input:focus-visible) {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.app-swatch:has(input:checked) {
    border-color: var(--text);
}

/* A white check on the picked swatch: every tone is dark, so white always reads. */
.app-swatch:has(input:checked)::after {
    content: '';
    position: absolute;
    inset: 0;
    width: 0.45rem;
    height: 0.8rem;
    margin: auto;
    border: solid var(--surface);
    border-width: 0 2px 2px 0;
    transform: translateY(-2px) rotate(45deg);
}

.app-swatch--slate { --swatch: var(--swatch-accent-1); }
.app-swatch--graphite { --swatch: var(--swatch-accent-2); }
.app-swatch--teal { --swatch: var(--swatch-accent-3); }
.app-swatch--forest { --swatch: var(--swatch-accent-4); }
.app-swatch--navy { --swatch: var(--swatch-accent-5); }
.app-swatch--burgundy { --swatch: var(--swatch-accent-6); }
.app-swatch--plum { --swatch: var(--swatch-accent-7); }
.app-swatch--bronze { --swatch: var(--swatch-accent-8); }
.app-swatch--black { --swatch: var(--swatch-text-1); }
.app-swatch--grey-1 { --swatch: var(--swatch-text-2); }
.app-swatch--grey-2 { --swatch: var(--swatch-text-3); }
.app-swatch--grey-3 { --swatch: var(--swatch-text-4); }
.app-swatch--grey-4 { --swatch: var(--swatch-text-5); }
.app-swatch--grey-5 { --swatch: var(--swatch-text-6); }

/* --- The drop zone for an image -------------------------------------------------------- */

/*
 * Logo, signature and stamp are dropped or clicked, not chosen from a bare file input: a dashed
 * box with an upload glyph and the words «Click or drop image». The real input stays inside the
 * label, hidden but reachable by keyboard, so a click anywhere in the box opens the file dialog.
 */
.app-drop {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-1);
    min-height: 72px;
    padding: var(--space-2);
    border: 1px dashed var(--border-control);
    border-radius: var(--radius-control);
    background: var(--surface);
    color: var(--text-3);
    font-size: var(--fs-caption);
    text-align: center;
    cursor: pointer;
    transition: background var(--transition), border-color var(--transition), color var(--transition);
}

.app-drop:hover,
.app-drop--over {
    border-color: var(--accent);
    background: var(--accent-soft);
    color: var(--accent);
}

.app-drop:has(input:focus-visible) {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.app-drop__icon {
    flex: none;
    width: var(--glyph);
    height: var(--glyph);
    fill: none;
    stroke: currentColor;
    stroke-width: var(--stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}

/*
 * A zone holding its picture (image-zone.js; drawn in no mockup, the owner's to approve — KINV-144 open 2):
 * a solid frame instead of the dashed one, the picture contained in the zone's own height, the words and the
 * glyph gone. The × is a sibling of the label (no button inside a label), so the field becomes one grid
 * column and the × shares the zone's cell, at its top right corner.
 */
.app-drop--filled {
    height: 72px;
    border-style: solid;
    padding: var(--space-0);
}

.app-drop--filled > .app-drop__icon,
.app-drop--filled > span {
    display: none;
}

.app-drop__image {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

.app-field:has(> .app-drop__remove) {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
}

.app-field:has(> .app-drop__remove) > * {
    grid-column: 1;
}

.app-field:has(> .app-drop__remove) > .app-drop {
    grid-row: 2;
}

.app-drop__remove {
    grid-row: 2;
    justify-self: end;
    align-self: start;
    margin: var(--space-0);
    font-size: var(--fs-section);
    line-height: var(--lh-flat);
}

/* A refused file: the zone takes the soft orange of an invalid control (D-086); the sentence stands below. */
.app-field--invalid .app-drop {
    border-color: var(--warn);
    background: var(--scale-warn-1);
}

/* A zone that takes no file now (the read-only window): the disabled tile's fill, its hover taken back. */
.app-drop:has(input:disabled),
.app-drop:has(input:disabled):hover {
    border-color: var(--border);
    background: var(--surface-2);
    color: var(--text-disabled);
    cursor: not-allowed;
}

/* --- The ordinary checkbox ------------------------------------------------------------- */

/*
 * A consent or an option is a box and a label the user clicks (owner, 23.10.2026: «people will
 * not understand where to click» on a tile). The native box, in the accent colour, with the
 * label beside it; the whole line is the target.
 */
.app-check {
    display: flex;
    align-items: flex-start;
    gap: var(--space-1);
    color: var(--text);
    cursor: pointer;
}

.app-check input {
    flex: none;
    width: var(--glyph);
    height: var(--glyph);
    margin: var(--glyph-nudge) 0 0;
    accent-color: var(--accent);
    cursor: pointer;
}

/*
 * A refused option keeps `aria-disabled`, not `disabled`, so the tooltip with the reason still shows
 * (D-118); the look says it is not waiting for a click: the muted text (--text-3 holds core.md §17,
 * --text-disabled does not) and the box in the disabled button's tokens (button.css).
 */
.app-check:has(input[aria-disabled='true']),
.app-check input[aria-disabled='true'] {
    cursor: not-allowed;
}

.app-check:has(input[aria-disabled='true']) {
    color: var(--text-3);
}

.app-check input[aria-disabled='true'] {
    border-color: var(--border);
    background: var(--surface-2);
}

/* The line under the last control of a payment form: the way back, quiet and centred. */
.app-form__foot {
    text-align: center;
}

/* --- Item lines: the old site's inline row --------------------------------------------- */

/*
 * Description · Qty · Unit price · Discount · ×, one row per item, every cell a field of its own,
 * so the text of a line is seen as typed (a narrow table cell cut it to «Website up»). The
 * description takes the room the figures do not need.
 */
.app-lines {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

/*
 * Qty is the project's narrow column (core.md §5.1, --w-col-narrow): «2,000,000.000», the largest
 * quantity with its places, is 91 px at --fs-dense plus 2 × --space-1 of padding and 2 px of border =
 * 109 px (KINV-128 §2, KINV-60). The rest shares what is left, the description most; the unit price
 * before the discount, so «900.00» still shows whole in the narrowest desktop form (1081 px: about 64 px).
 */
.app-lines__row {
    display: grid;
    grid-template-columns: minmax(0, 2.4fr) var(--w-col-narrow) minmax(0, 1.2fr) minmax(0, 1fr) var(--icon-btn);
    gap: var(--space-0);
    align-items: center;
}

.app-lines__row--head {
    color: var(--text-3);
    font-size: var(--fs-caption);
    font-weight: var(--fw-semibold);
}

.app-lines .app-field__control {
    width: 100%;
    min-width: 0;
    height: var(--control);
    padding-inline: var(--space-1);
    /* Denser than the 15px floor, so more of a long description shows (owner, 23.10.2026). */
    font-size: var(--fs-dense);
}

/*
 * A group refused whole by the PRO read-only window (the editor А.Б2, core.md §9.5): `aria-disabled` on the
 * group, never `disabled` on its controls, which the live sheet and the leave guard would skip. The choice made
 * stays readable; the others go quiet and nothing answers the pointer.
 */
[aria-disabled='true'] .app-tile,
[aria-disabled='true'] .app-swatch,
[aria-disabled='true'] .app-check {
    cursor: not-allowed;
}

[aria-disabled='true'] .app-tile:not(:has(input:checked)),
[aria-disabled='true'] .app-tile:not(:has(input:checked)):hover {
    border-color: var(--tile-edge, var(--border-control));
    background: var(--surface);
    color: var(--text-3);
}

[aria-disabled='true'] .app-swatch:not(:has(input:checked)):hover {
    border-color: var(--border);
}

/*
 * An editor with its preview (Ж.Б12 article, the legal page; KINV-255, owner 30.10.2026).
 *
 * The fields stand in the left column at the form's width, the preview to the right of them, level with
 * the top; below the desktop breakpoint they stack and the preview goes under the fields it mirrors.
 * 1081px = --bp-desktop + 1 (var() does not work in a media condition).
 */
.app-form__main {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    min-width: 0;
}

@media (min-width: 1081px) {
    .app-form--split {
        display: grid;
        grid-template-columns: minmax(0, var(--w-form)) minmax(0, 1fr);
        align-items: start;
        gap: var(--space-6);
        max-width: none;
    }
}
