/*
    The whole of this application's styling.

    There is no component suite behind it — see
    docs/ui/component-library-decision-record.md for why, and for the cost this
    file is the accepted half of. What that decision buys is control over the one
    thing the users will actually judge this port on: density. The application
    being replaced is a WinForms client whose grid rows are about 22 pixels tall
    and whose forms put a label directly against its box. Anything appreciably
    airier reads to those users as a different, slower program.

    So the numbers here are deliberate and small, and they are in one place. Change
    --row-height and every table in the application moves together.

    Light only. A staff application used at desks under office lighting has no dark
    mode requirement, and a second palette is a second set of contrast ratios to
    get right with nobody asking for it.
*/

:root {
    /*
        Type. 13.5px is the size a dense desktop grid is read at; the browser
        default of 16px is a reading size for prose and puts a third fewer rows on
        the screen. The system stack means Segoe UI on the Windows machines these
        users are on — the font the application they know is drawn in.
    */
    --font: -apple-system, "Segoe UI", system-ui, sans-serif;
    --font-mono: "Cascadia Mono", "Consolas", ui-monospace, monospace;
    --text: 13.5px;
    --text-small: 12px;
    --text-heading: 1.15rem;

    /*
        Spacing. A four-pixel grid: everything is a multiple, so nothing is
        eyeballed and density stays even across screens written by different hands.
    */
    --gap-tight: 4px;
    --gap: 8px;
    --gap-wide: 16px;
    --gap-section: 24px;

    /* Grid and control metrics — the density dial. */
    --row-height: 26px;
    --control-height: 28px;
    --radius: 3px;

    /*
        Neutrals, warm-free. Grey with a blue cast reads as a business
        application rather than a document.
    */
    --page: #f3f4f6;
    --surface: #ffffff;
    --ink: #18181b;
    --ink-muted: #52525b;
    --ink-faint: #71717a;
    --line: #d4d4d8;
    --line-strong: #a1a1aa;
    --hover: #eef2ff;
    --stripe: #fafafa;

    /*
        One accent, used for what is selected, focused or primary — and for
        nothing else. A palette with several accents makes the user work out
        which colour means what.
    */
    --accent: #1d4ed8;
    --accent-dark: #1e40af;
    --accent-ink: #ffffff;
    --accent-wash: #dbeafe;

    --danger: #991b1b;
    --danger-dark: #7f1d1d;
    --danger-wash: #fee2e2;
    --danger-line: #fca5a5;
    --warning-wash: #fef3c7;
    --warning-line: #fcd34d;

    /* Chrome. */
    --sidebar-width: 208px;
    --header-height: 44px;
}

* {
    box-sizing: border-box;
}

html,
body {
    height: 100%;
}

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

h1 {
    margin: 0 0 var(--gap-wide);
    font-size: var(--text-heading);
    font-weight: 600;
}

h2 {
    margin: var(--gap-section) 0 var(--gap);
    font-size: 1rem;
    font-weight: 600;
}

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

code {
    font-family: var(--font-mono);
    font-size: var(--text-small);
}

/*
    One focus ring, on everything, and never removed. Keyboard use is a stated
    requirement of this port: these users drive the desktop app by tab and arrow
    keys, and a focus outline they cannot see is the fastest way to lose them.
    :focus-visible rather than :focus so a mouse click does not leave a ring
    behind it.
*/
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

/* ---------------------------------------------------------------- chrome -- */

.shell {
    display: grid;
    grid-template-columns: var(--sidebar-width) 1fr;
    grid-template-rows: var(--header-height) 1fr;
    grid-template-areas:
        "brand header"
        "nav content";
    height: 100vh;
}

.shell-brand {
    grid-area: brand;
    display: flex;
    align-items: center;
    gap: var(--gap);
    padding: 0 var(--gap-wide);
    background: var(--accent-dark);
    color: var(--accent-ink);
    font-weight: 600;
    letter-spacing: 0.02em;
}

.shell-brand a {
    color: inherit;
    text-decoration: none;
}

.shell-header {
    grid-area: header;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--gap-wide);
    padding: 0 var(--gap-wide);
    background: var(--surface);
    border-bottom: 1px solid var(--line);
}

.shell-version {
    color: var(--ink-faint);
    font-size: var(--text-small);
}

.shell-user {
    color: var(--ink-muted);
}

.shell-nav {
    grid-area: nav;
    overflow-y: auto;
    padding: var(--gap) 0;
    background: var(--surface);
    border-right: 1px solid var(--line);
}

.shell-content {
    grid-area: content;
    overflow: auto;
    padding: var(--gap-wide) var(--gap-section);
}

/*
    Below this the sidebar stops paying for itself. The application is used at
    desks and this is a fallback, not a mobile design: the chrome stacks, the
    navigation becomes a row, and the content keeps the full width.
*/
@media (max-width: 720px) {
    .shell {
        grid-template-columns: 1fr;
        grid-template-rows: var(--header-height) auto 1fr;
        grid-template-areas:
            "brand"
            "nav"
            "content";
    }

    .shell-header {
        display: none;
    }

    .shell-nav {
        display: flex;
        flex-wrap: wrap;
        padding: var(--gap-tight);
        border-right: 0;
        border-bottom: 1px solid var(--line);
    }

    .shell-content {
        padding: var(--gap-wide);
    }
}

/* ------------------------------------------------------------ navigation -- */

.nav-link {
    display: block;
    padding: 6px var(--gap-wide);
    color: var(--ink);
    text-decoration: none;
    border-left: 3px solid transparent;
}

.nav-link:hover {
    background: var(--hover);
}

/*
    NavLink adds .active on the entry matching the current route. Colour alone
    would not carry it for a user who cannot separate these two blues, so the
    marker is a bar and a weight change as well.
*/
.nav-link.active {
    background: var(--accent-wash);
    border-left-color: var(--accent);
    font-weight: 600;
}

.nav-sign-out {
    margin-top: var(--gap);
    padding-top: var(--gap);
    border-top: 1px solid var(--line);
}

/* ---------------------------------------------------------------- tables -- */

/*
    Every table in the application, including the ones QuickGrid renders — it
    emits a plain <table class="quickgrid">, so it inherits all of this and the
    class is there for the few rules that are its own.

    Dense, ruled, striped, with a header that stays put when the body scrolls.
    Borders rather than whitespace to separate columns: whitespace-separated
    tables need to be airy to stay readable, and airy is the thing being avoided.
*/
table {
    width: 100%;
    border-collapse: collapse;
    background: var(--surface);
    font-variant-numeric: tabular-nums;
}

thead th {
    position: sticky;
    top: 0;
    z-index: 1;
    padding: var(--gap-tight) var(--gap);
    text-align: left;
    font-weight: 600;
    white-space: nowrap;
    background: var(--page);
    border-bottom: 1px solid var(--line-strong);
}

tbody td {
    height: var(--row-height);
    padding: 0 var(--gap);
    border-bottom: 1px solid var(--line);
}

tbody tr:nth-child(even) {
    background: var(--stripe);
}

tbody tr:hover {
    background: var(--hover);
}

/*
    Row selection, and the row the keyboard is on. FullRowSelect was how every
    list in the desktop app behaved; aria-selected is what carries it to assistive
    technology, so the styling hangs off that rather than off a class somebody has
    to remember to keep in step.
*/
tbody tr[aria-selected="true"] {
    background: var(--accent-wash);
}

/*
    The same, for the grids QuickGrid renders. It builds its own <tr> and offers no
    way to put an attribute on it — RowClass is the only hook there is — so those
    carry a class instead. The rule above stays for every table this application
    writes itself.
*/
tbody tr.selected {
    background: var(--accent-wash);
}

/* Numbers right, so digits line up under each other and are comparable. */
.numeric {
    text-align: right;
}

/* ----------------------------------------------------------------- grids -- */

/*
    QuickGrid's own markup, dressed in this application's clothes.

    Neither stylesheet the package ships is linked. One is scoped CSS, which
    arrives only through the .styles.css bundle and brings every other component
    library's along with it; the other is a theme with its own opinion about
    padding, which is the one thing this file exists to decide. What is left to do
    is the handful of rules its header needs — the table itself already inherits
    everything above.
*/

.grid-toolbar {
    display: flex;
    align-items: baseline;
    gap: var(--gap);
    margin-bottom: var(--gap);
}

.grid-toolbar label {
    margin: 0;
}

.grid-toolbar input {
    width: 18rem;
}

/* Waiting, and having nothing to show, both belong under the table. */
.grid-status {
    margin: var(--gap) 0;
}

.quickgrid .col-header-content {
    display: flex;
    align-items: center;
    gap: var(--gap-tight);
}

/*
    A sortable header is a <button> — which is right, because clicking it does
    something and a keyboard has to be able to reach it. It just must not look like
    one sitting in a header cell.
*/
.quickgrid button.col-title {
    display: flex;
    align-items: center;
    gap: var(--gap-tight);
    width: 100%;
    height: auto;
    padding: 0;
    font: inherit;
    font-weight: 600;
    color: inherit;
    text-align: left;
    background: none;
    border: 0;
    border-radius: 0;
    cursor: pointer;
}

.quickgrid button.col-title:hover {
    background: none;
    text-decoration: underline;
}

.quickgrid .col-title-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/*
    Which way a sorted column is sorted. Drawn in borders rather than fetched as an
    image, so it takes the ink colour and costs no request. The box is always the
    same size whether or not there is a triangle in it, so sorting a column does
    not change its width.
*/
.quickgrid .sort-indicator {
    position: relative;
    flex: none;
    width: 8px;
    height: 8px;
}

.quickgrid th.col-sort-asc .sort-indicator::after,
.quickgrid th.col-sort-desc .sort-indicator::after {
    content: "";
    position: absolute;
    top: 2px;
    left: 0;
    border-right: 4px solid transparent;
    border-left: 4px solid transparent;
}

.quickgrid th.col-sort-asc .sort-indicator::after {
    border-bottom: 5px solid var(--ink-muted);
}

.quickgrid th.col-sort-desc .sort-indicator::after {
    border-top: 5px solid var(--ink-muted);
}

/*
    The cell that opens the row, and the row's one tab stop. A real button, because
    that is what it is — but drawn as the value it shows, so the table still reads
    as a table rather than as a column of buttons.
*/
.row-open {
    height: auto;
    padding: 0;
    font: inherit;
    color: var(--accent);
    text-align: left;
    background: none;
    border: 0;
    border-radius: 0;
    cursor: pointer;
}

.row-open:hover {
    background: none;
    text-decoration: underline;
}

/*
    QuickGrid's paginator. Its four buttons hold no text at all — they are drawn
    entirely by the theme that is not linked — so the arrows are here. Each one
    already carries an aria-label, which is what actually says where it goes.
*/
.paginator {
    display: flex;
    align-items: center;
    gap: var(--gap);
    margin-top: var(--gap);
}

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

.paginator nav {
    display: flex;
    gap: var(--gap-tight);
    margin-left: auto;
}

.paginator nav button {
    width: var(--control-height);
    padding: 0;
}

.paginator nav button[disabled] {
    color: var(--ink-faint);
    cursor: default;
}

.paginator .go-first::before {
    content: "«";
}

.paginator .go-previous::before {
    content: "‹";
}

.paginator .go-next::before {
    content: "›";
}

.paginator .go-last::before {
    content: "»";
}

/* ----------------------------------------------------------------- forms -- */

label {
    display: block;
    margin-bottom: 2px;
    font-weight: 600;
}

input,
select,
textarea {
    width: 100%;
    height: var(--control-height);
    padding: 0 6px;
    font: inherit;
    color: inherit;
    background: var(--surface);
    border: 1px solid var(--line-strong);
    border-radius: var(--radius);
}

textarea {
    height: auto;
    padding: 4px 6px;
}

input[type="checkbox"] {
    width: auto;
    height: auto;
    margin-right: var(--gap-tight);
}

input[readonly],
textarea[readonly],
input:disabled,
select:disabled {
    color: var(--ink-muted);
    background: var(--page);
}

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

/*
    A named group of fields — the GroupBox the desktop forms fenced their sections
    off with: "Main Contact" on frmEditCustomer, and the two around its child
    grids. A customer form without them is one undifferentiated column of boxes,
    and which box belongs to the customer and which to the address it is filed
    under stops being visible.

    A real <fieldset> and <legend> rather than a div and a heading, because the
    grouping is then announced rather than only drawn: a screen reader reads the
    legend before the fields inside it.
*/
.subform {
    margin-bottom: var(--gap-wide);
    padding: var(--gap-wide);
    border: 1px solid var(--line);
    border-radius: var(--radius);
}

.subform > legend {
    padding: 0 var(--gap);
    font-weight: 600;
}

/*
    A row of tick boxes under one caption — the three that make up an address
    type. The block, bold label rule above is for a box with a name over it, and
    these are the other thing: the words sit beside their box and belong to it,
    so they read as choices rather than as three more fields.
*/
.checkboxes label {
    display: inline-flex;
    align-items: center;
    margin-right: var(--gap-wide);
    font-weight: 400;
}

/*
    Choosing a file. The browser draws its own button inside this input and sizes
    it itself, so the full-width, fixed-height rule above is undone rather than
    fought with.
*/
.upload {
    margin-bottom: var(--gap);
}

.upload input[type="file"] {
    width: auto;
    height: auto;
    padding: 4px;
}

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

button,
.button {
    display: inline-block;
    height: var(--control-height);
    padding: 0 14px;
    font: inherit;
    color: var(--ink);
    text-decoration: none;
    background: var(--surface);
    border: 1px solid var(--line-strong);
    border-radius: var(--radius);
    cursor: pointer;
}

button:hover,
.button:hover {
    background: var(--hover);
}

button[type="submit"],
.button-primary {
    color: var(--accent-ink);
    background: var(--accent);
    border-color: var(--accent);
    font-weight: 600;
}

button[type="submit"]:hover,
.button-primary:hover {
    background: var(--accent-dark);
}

/*
    The button that deletes something. Red is the only place in this application
    where colour carries meaning on its own, so it is never the only signal — the
    prompt it sits in has already said in words what is about to happen, and the
    button is named for the action rather than "Yes".
*/
.button-danger {
    color: var(--accent-ink);
    background: var(--danger);
    border-color: var(--danger);
    font-weight: 600;
}

.button-danger:hover {
    background: var(--danger-dark);
}

/*
    A module's row of buttons, above whatever it acts on. Every screen in the
    desktop app had one — the four along the bottom of frmUsers, the five on
    frmWorksheets — and they are all the same shape, so this is one rule rather
    than one per module.

    A disabled button is dimmed rather than removed. Which actions a screen offers
    is part of how a user learns it, and one that appears only once the right row
    is selected cannot be found by looking.
*/
.toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--gap);
    margin-bottom: var(--gap-wide);
}

button:disabled,
.button:disabled {
    color: var(--ink-faint);
    background: var(--page);
    border-color: var(--line);
    cursor: default;
}

button:disabled:hover,
.button:disabled:hover {
    background: var(--page);
}

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

.problem,
.notice {
    margin: var(--gap) 0;
    padding: var(--gap) var(--gap-wide);
    border: 1px solid;
    border-radius: var(--radius);
}

.problem {
    color: var(--danger);
    background: var(--danger-wash);
    border-color: var(--danger-line);
}

.notice {
    background: var(--warning-wash);
    border-color: var(--warning-line);
}

/* The validation summary is a list of problems; it needs no bullets. */
.problem ul {
    margin: 0;
    padding-left: var(--gap-wide);
}

.muted {
    color: var(--ink-muted);
}

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

/*
    The home page: the six buttons frmMain opened its modules with, as a grid of
    targets big enough to hit without aiming. This is the one place in the
    application that is deliberately not dense — it is a menu, read once at the
    start of a session, not a working surface.
*/
.card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
    gap: var(--gap-wide);
    max-width: 60rem;
}

.card {
    display: block;
    padding: var(--gap-wide);
    color: inherit;
    text-decoration: none;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
}

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

.card strong {
    display: block;
    margin-bottom: var(--gap-tight);
    color: var(--accent);
}

.card span {
    color: var(--ink-muted);
    font-size: var(--text-small);
}

/* ----------------------------------------------------- the account pages -- */

/*
    Signing in, signing out and setting a password. These render before there is a
    shell to put them in — no navigation, nothing to navigate to — so they get a
    card in the middle of an empty page instead.
*/
.account {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    padding: var(--gap-wide);
}

.account-card {
    width: 100%;
    max-width: 22rem;
    padding: var(--gap-section);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 6px;
}

.account-card h1 {
    margin-bottom: var(--gap-section);
    text-align: center;
    letter-spacing: 0.04em;
}

.account-card button {
    width: 100%;
    margin-top: var(--gap-wide);
}

/* ---------------------------------------------------------- busy and gone -- */

/*
    The busy indicator the shared components use. Defined here so that every screen
    that waits looks the same while it does, and so that there is one answer to
    "what does waiting look like" rather than one per module.
*/
.busy {
    display: inline-block;
    width: 14px;
    height: 14px;
    vertical-align: -2px;
    border: 2px solid var(--line-strong);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
}

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

/*
    Anyone who has turned motion down has said they do not want this. Hold the
    ring still rather than removing it — it is the only sign the screen is doing
    something.
*/
@media (prefers-reduced-motion: reduce) {
    .busy {
        animation: none;
    }
}

/*
    The ring with its message beside it, which is what BusyIndicator renders. The
    ring alone says something is happening; the words say what, and they are the
    half a screen reader can use.
*/
.busy-line {
    display: inline-flex;
    align-items: center;
    gap: var(--gap);
    color: var(--ink-muted);
}

/*
    Blazor's own bar for an error inside a circuit, which is a different failure
    from the one the ErrorBoundary catches: by the time this shows, the circuit is
    already unusable and reloading is the only way on. Blazor unhides it; it is
    hidden here so it does not sit on every page.
*/
#blazor-error-ui {
    display: none;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1000;
    padding: var(--gap) var(--gap-wide);
    color: var(--danger);
    background: var(--warning-wash);
    border-top: 1px solid var(--warning-line);
}

#blazor-error-ui .dismiss {
    float: right;
    cursor: pointer;
}

/* --------------------------------------------------------------- dialogs -- */

/*
    The 15 modal windows of the desktop app become <dialog>. Focus trapping, Esc
    to close and the backdrop are the element's own; what is here is the shape,
    which ModalDialog fills in. The reconnect dialog is a <dialog> too and picks
    these up without being one of the fifteen.
*/
dialog {
    padding: 0;
    color: var(--ink);
    background: var(--surface);
    border: 1px solid var(--line-strong);
    border-radius: 6px;
    box-shadow: 0 8px 32px rgb(0 0 0 / 25%);
}

/*
    ModalDialog's own shape, which the reconnect overlay below is deliberately not
    given. A dialog is as wide as what is in it, up to the window: the worksheet
    editor is the case that forces a ceiling, because its line grid is eleven
    columns and left to itself the dialog would grow past the right-hand edge of
    the screen and take the Save button with it. Capped rather than fixed, so the
    delete prompt and the item picker stay the size of their own contents instead
    of all being sized for the largest one.

    The title and the buttons then stay put and the body between them scrolls,
    because the alternative is a dialog whose Save button is below the fold on a
    long worksheet — the one button the user is looking for.
*/
.app-dialog[open] {
    display: flex;
    flex-direction: column;
    max-width: min(96vw, 1400px);
    max-height: 92vh;
    overflow: hidden;
}

dialog::backdrop {
    background: rgb(0 0 0 / 35%);
}

.dialog-title {
    padding: var(--gap) var(--gap-wide);
    font-weight: 600;
    background: var(--page);
    border-bottom: 1px solid var(--line);
}

.dialog-body {
    padding: var(--gap-wide);

    /* The scrolling half of the rule above: this is the part that gives. */
    overflow: auto;
}

.dialog-buttons {
    display: flex;
    justify-content: flex-end;
    gap: var(--gap);
    padding: var(--gap) var(--gap-wide);
    background: var(--page);
    border-top: 1px solid var(--line);
}

/* ------------------------------------------------------------- reconnect -- */

/*
    A dropped circuit. This is the failure mode of Blazor Server that a user will
    actually meet — a laptop lid closing, a wifi handover, an App Service restart —
    and if nothing says so the screen simply stops responding to clicks, which
    reads as the application having broken.

    The classes are Blazor's: it puts one of components-reconnect-{show,retrying,
    failed,paused,resume-failed} on the dialog and drives everything from there,
    including the countdown it writes into #components-seconds-to-next-attempt.
    Each message is hidden by default and shown by exactly one of those states.
*/
#components-reconnect-modal {
    max-width: 24rem;
    padding: var(--gap-section);
    text-align: center;
}

.components-reconnect-first-attempt-visible,
.components-reconnect-repeated-attempt-visible,
.components-reconnect-failed-visible,
.components-pause-visible,
.components-resume-failed-visible,
.components-rejoining-animation {
    display: none;
}

#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible {
    display: block;
}

#components-reconnect-modal .busy {
    width: 20px;
    height: 20px;
    margin: 0 auto var(--gap-wide);
}

#components-reconnect-modal button {
    margin-top: var(--gap-wide);
}

/* --------------------------------------------------------------- modules -- */

/*
    The row of controls above a grid — search box, filter, and the buttons that
    act on the selected row. This is frmWorksheets' own top strip and frmItems'
    after it: everything on one line, labels beside their boxes rather than above
    them, and the buttons pushed to the right where the desktop forms put them.

    Wraps rather than scrolls. On a narrow window the buttons drop to a second
    line and stay reachable, where a horizontal scrollbar would hide the Delete
    button behind a gesture nobody makes on a form.
*/
.toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--gap);
    margin-bottom: var(--gap);
}

/* Labels sit beside their control here, not over it. */
.toolbar label {
    margin: 0;
}

.toolbar input[type="search"] {
    width: 18rem;
}

.toolbar select {
    width: auto;
}

/* What separates the two halves: what you type into, and what you press. */
.toolbar-spacer {
    margin-left: auto;
}

/* The running totals under the line grid. */
.totals {
    font-size: var(--text-small);
}

/*
    The worksheet header — three blocks of fields side by side: what identifies
    the worksheet, where the load comes from, where it goes. frmWorksheet grouped
    them the same way and in the same order, because that is the order the office
    fills them in.

    auto-fit rather than three fixed columns, so a narrow window stacks them
    instead of squeezing every box down to something unreadable.
*/
.form-columns {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 0 var(--gap-section);
}

/* The first heading in each block sits against the top, not a line below it. */
.form-columns h2:first-child {
    margin-top: 0;
}

/*
    The four lifecycle boxes, on one line and in their order. A fieldset because
    they are one question about one worksheet — a screen reader announces the
    legend with each box, so "Status: Delivered" rather than a bare "Delivered"
    floating between a delivery date and a grid.
*/
.lifecycle {
    display: flex;
    flex-wrap: wrap;
    gap: var(--gap-wide);
    margin: var(--gap-wide) 0;
    padding: var(--gap) var(--gap-wide);
    border: 1px solid var(--line);
    border-radius: var(--radius);
}

.lifecycle legend {
    padding: 0 var(--gap-tight);
    font-weight: 600;
}

/*
    A checkbox and its words, as one target. The global rule makes every label a
    block above its control, which is right for a text box and wrong for this —
    here the label is the thing you click.
*/
.checkbox {
    display: inline-flex;
    align-items: center;
    margin: 0;
    font-weight: normal;
    white-space: nowrap;
}

/*
    A box inside a grid cell. The three editable columns on a worksheet line are
    all numeric and all short, so they are sized to their content rather than
    stretched to the column: a quantity box wide enough for a UPC makes the grid
    twice as wide as the numbers in it.
*/
input.cell {
    width: 7rem;
    height: calc(var(--row-height) - 4px);
    padding: 0 4px;
    text-align: right;
}

/*
    Taking a line off the worksheet. Drawn like the cell that opens a row rather
    than like a button, so a grid of ten lines is not a column of ten buttons —
    but in the danger colour, because it is the one control in the table that
    destroys something.
*/
.row-remove {
    height: auto;
    padding: 0;
    font: inherit;
    color: var(--danger);
    background: none;
    border: 0;
    border-radius: 0;
    cursor: pointer;
}

.row-remove:hover {
    background: none;
    text-decoration: underline;
}
