﻿/* Tanzanair Portal
 *
 * Palette is derived from the logo mark (#192262) rather than chosen around
 * it. Amber is reserved strictly for cautions, because amber is the caution
 * colour in every cockpit - that is a reason, not a preference. Violet
 * appears exactly once, on the primary action.
 *
 * Operational data - routes, distances, briefing numbers, coordinates - is
 * always set in monospace with tabular figures. Every document in this
 * business is a form with columns of figures, and the interface reads the
 * same way.
 */

:root {
    color-scheme: light dark;

    --ink: #0b0f2b;
    --brand: #192262;
    --lift: #2e3a8f;
    --violet: #6c5ce7;
    --paper: #f5f6fa;
    --caution: #ffb020;

    /* Amber says look at this; red says this is wrong and must not stay.
     * Reserved for exactly that, so it keeps meaning it. */
    --alarm: #ff5a4e;

    /* Swap --font-display to 'Biome' the day the webfont licence lands.
     * One line, no other change. */
    --font-display: 'Outfit', system-ui, -apple-system, 'Segoe UI', sans-serif;
    --font-body: 'Outfit', system-ui, -apple-system, 'Segoe UI', sans-serif;
    --font-data: ui-monospace, 'SF Mono', 'Cascadia Mono', 'Roboto Mono', 'Consolas', monospace;

    --step: 0.5rem;
    --radius: 10px;
    --edge: 1px solid rgb(255 255 255 / 0.12);
}

/* Dark is the default: this is a tool used at 2am as often as at 2pm. */
:root,
:root[data-theme='dark'] {
    --bg: var(--ink);
    --bg-deep: #070a1e;
    --surface: rgb(255 255 255 / 0.055);
    --surface-hover: rgb(255 255 255 / 0.075);
    --border: rgb(255 255 255 / 0.12);
    --text: #eef0f8;
    --text-dim: #a3a9c9;
    --text-faint: #6b7299;
    --arc: rgb(140 128 240 / 0.42);
    /* Neon on dark: cyan-violet, the colour of a moving-map display. */
    --neon: rgb(124 214 255 / 0.55);
    --neon-soft: rgb(124 214 255 / 0.3);
    --neon-ship: rgb(168 232 255 / 0.9);
    --arc-faint: rgb(140 128 240 / 0.09);
    --chart-self: rgb(255 255 255 / 0.07);
    --chart-void: rgb(255 255 255 / 0.028);
}

:root[data-theme='light'] {
    --bg: var(--paper);
    --bg-deep: #e8eaf3;
    --surface: rgb(255 255 255 / 0.75);
    --surface-hover: #ffffff;
    --border: rgb(25 34 98 / 0.14);
    --text: #131735;
    --text-dim: #4a5178;
    --text-faint: #7c83a6;
    --arc: rgb(25 34 98 / 0.4);
    --arc-faint: rgb(25 34 98 / 0.1);
    /* Neon on white. A glow cannot brighten paper, so the light theme goes
     * the other way: a saturated teal ink, denser than the dark theme's, with
     * the blur reading as a soft halo around it rather than as light. */
    --neon: rgb(0 132 168 / 0.5);
    --neon-soft: rgb(0 132 168 / 0.26);
    --neon-ship: rgb(0 108 142 / 0.85);
    --edge: 1px solid rgb(25 34 98 / 0.1);
    --chart-self: rgb(25 34 98 / 0.1);
    --chart-void: rgb(25 34 98 / 0.045);
}

@media (prefers-color-scheme: light) {
    :root:not([data-theme]) {
        --bg: var(--paper);
        --bg-deep: #e8eaf3;
        --surface: rgb(255 255 255 / 0.75);
        --surface-hover: #ffffff;
        --border: rgb(25 34 98 / 0.14);
        --text: #131735;
        --text-dim: #4a5178;
        --text-faint: #7c83a6;
        --arc: rgb(25 34 98 / 0.4);
        --arc-faint: rgb(25 34 98 / 0.1);
        /* The same teal the explicit light theme uses. Left out at first,
         * and the effect was that anyone on Auto with a light desktop got
         * the dark theme's cyan drawn on white paper - a map that was
         * technically there and could not be seen. */
        --neon: rgb(0 132 168 / 0.5);
        --neon-soft: rgb(0 132 168 / 0.26);
        --neon-ship: rgb(0 108 142 / 0.85);
        --edge: 1px solid rgb(25 34 98 / 0.1);
    --chart-self: rgb(25 34 98 / 0.1);
    --chart-void: rgb(25 34 98 / 0.045);
    }
}

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

html {
    -webkit-text-size-adjust: 100%;
}

body {
    margin: 0;
    min-height: 100dvh;
    font-family: var(--font-body);
    font-size: 16px;
    line-height: 1.55;
    color: var(--text);
    background: var(--bg);
    background-image:
        radial-gradient(120% 80% at 50% -10%, rgb(46 58 143 / 0.35), transparent 60%),
        radial-gradient(90% 60% at 50% 110%, rgb(25 34 98 / 0.3), transparent 60%);
    background-attachment: fixed;

    /* clip, not hidden. "hidden" on one axis makes the other compute to
     * "auto", which turns body into a scroll container that never scrolls -
     * and any position:sticky descendant then anchors to it and never
     * sticks. "clip" trims the overflow without creating that box. */
    overflow-x: clip;
}

:root[data-theme='light'] body {
    background-image:
        radial-gradient(120% 80% at 50% -10%, rgb(46 58 143 / 0.1), transparent 60%);
}

@media (prefers-color-scheme: light) {
    :root:not([data-theme]) body {
        background-image:
            radial-gradient(120% 80% at 50% -10%, rgb(46 58 143 / 0.1), transparent 60%);
    }
}

/* ---------------------------------------------------------------- signature
 *
 * A great-circle arc traced from real coordinates in the portal's own
 * database - Dar es Salaam out to a bush strip and back. Not decoration:
 * it is the thing this application computes, drawn slowly behind the glass.
 */

.field {
    position: fixed;
    inset: 0;

    /* Behind everything, not level with it.
     *
     * At 0 this is a positioned element and the content is not, so it painted
     * *above* the page - invisible wherever a panel is opaque, and a neon
     * border straight through the middle of a name wherever one is not. On
     * the light theme the surfaces are translucent, so it drew itself over
     * every table on the screen.
     *
     * -1 puts it behind normal flow, which is where a backdrop belongs. */
    z-index: -1;
    pointer-events: none;
    /* Clipped. The arc is drawn at whatever width the script last saw,
     * and a fixed element whose contents spill still lengthens the
     * document - so on a phone the decoration was putting a horizontal
     * scrollbar on pages that otherwise fitted perfectly well. */
    overflow: hidden;
}

.field svg {
    width: 100%;
    height: 100%;
    display: block;
}

.field .grid line {
    stroke: var(--arc-faint);
    stroke-width: 0.5;
}

/* A track drawn the way a planned leg appears on a navigation display: a
 * continuous thin line with a marker drifting along it.
 *
 * The line is solid rather than dashed because it sits behind glass. A
 * dashed 2px line under a backdrop blur is annihilated - measured, not
 * assumed - and an invisible signature is not restraint, it is absence.
 * The panel blur was reduced to match; glass you cannot see through is
 * just a grey rectangle. */
/* ------------------------------------------------------------- the map
 *
 * Tanzania in neon, behind everything.
 *
 * Two different problems in the two themes, and one colour will not solve
 * both. Neon works on dark because the glow is lighter than what surrounds
 * it; on white, a light glow is invisible and a bright one turns to haze,
 * so the light theme uses a deeper, denser line and leans on the ink rather
 * than the light. Same drawing, opposite physics.
 */
.field .map path {
    fill: none;
    stroke: var(--neon);
    stroke-width: 1.6;
    stroke-linejoin: round;
    stroke-linecap: round;
}

.field .map .border {
    stroke-width: 2;
}

/* The lake and the islands sit back from the coastline so the country's
 * outline stays the thing being read. */
.field .map .water,
.field .map .isle {
    stroke: var(--neon-soft);
    stroke-width: 1.3;
}

/* Inside the borders: lakes, four rivers and three roads.
 *
 * Fainter than the coastline and with no glow of their own. The country's
 * edge is the drawing; this is texture underneath it, and the moment it
 * competes the whole thing reads as a busy atlas rather than a backdrop. */
.field .inland path {
    fill: none;
    stroke: var(--neon-soft);
    stroke-width: 1;
    stroke-linejoin: round;
    stroke-linecap: round;
    opacity: 0.85;
}

.field .inland .river {
    stroke-width: 0.9;
    opacity: 0.7;
}

/* Faintest of all, and dashed - a road is not a river and should not read
 * as one at a glance. */
.field .inland .road {
    stroke-width: 0.8;
    stroke-dasharray: 3 4;
    opacity: 0.5;
}

/* 5H-DJS. Filled rather than outlined - at this size an outlined aeroplane
 * is a smudge and a solid one is an aeroplane. */
.field .ship path {
    fill: var(--neon-ship);
    stroke: var(--neon-ship);
    stroke-width: 0.5;
    stroke-linejoin: round;
}

/* The planned track. It draws itself on rather than appearing whole,
 * because a route that is simply there reads as wallpaper and a route being
 * laid down reads as an aeroplane going somewhere. The dash figures are set
 * by the script from the path's own measured length - guessing them makes
 * the line finish early on a phone and late on a monitor. */
.field .arc {
    fill: none;
    stroke: var(--arc);
    stroke-width: 1.5;
    stroke-linecap: round;
}

/* The whole drawing leaves before the next route arrives, so one trip ends
 * before another starts instead of both being half-present at once. */
.field svg {
    opacity: 1;
    transition: opacity 900ms ease;
}

.field svg.leaving {
    opacity: 0;
}

/* An airfield appears as the track reaches it. The script sets the delay
 * per node from how far along the route that airfield sits. */
.field .node {
    transform-box: fill-box;
    transform-origin: center;
}

@media (prefers-reduced-motion: no-preference) {
    .field .node {
        animation: arrive 700ms ease-out both;
    }
}

@keyframes arrive {
    from { opacity: 0; transform: scale(0.2); }
    to   { opacity: 0.9; transform: scale(1); }
}

.field .runner {
    fill: none;
    stroke: var(--violet);
    stroke-width: 2.5;
    stroke-linecap: round;
    stroke-dasharray: 26 3000;
    animation: drift 14s linear infinite;
}

.field .node {
    fill: var(--arc);
    opacity: 0.9;
}

.field .node.origin {
    fill: var(--violet);
    opacity: 1;
}

@keyframes drift {
    from { stroke-dashoffset: 26; }
    to { stroke-dashoffset: -1400; }
}

/* ------------------------------------------------------------------ layout */

.shell {
    position: relative;
    z-index: 1;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    padding: calc(var(--step) * 3);
    gap: calc(var(--step) * 4);
}

.shell > header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: calc(var(--step) * 2);
}

.middle {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: calc(var(--step) * 2.5);
    width: 100%;
    max-width: 62rem;
    margin-inline: auto;
}

/* -------------------------------------------------------------------- mark */

.mark {
    display: flex;
    align-items: center;
    gap: calc(var(--step) * 1.5);
    text-decoration: none;
    color: inherit;
}

.mark img {
    height: 34px;
    width: auto;
    display: block;
}

:root[data-theme='light'] .mark img.for-dark { display: none; }
:root[data-theme='light'] .mark img.for-light { display: block; }
.mark img.for-light { display: none; }

@media (prefers-color-scheme: light) {
    :root:not([data-theme]) .mark img.for-dark { display: none; }
    :root:not([data-theme]) .mark img.for-light { display: block; }
}

.established {
    font-family: var(--font-data);
    font-size: 0.6875rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--text-faint);
    white-space: nowrap;
}

/* ------------------------------------------------------------------- title */

.title {
    text-align: center;
    max-width: 34rem;
}

.title h1 {
    margin: 0 0 calc(var(--step) * 1.5);
    font-family: var(--font-display);
    font-size: clamp(1.75rem, 5vw, 2.75rem);
    font-weight: 600;
    letter-spacing: -0.02em;
    line-height: 1.1;
}

.title p {
    margin: 0;
    color: var(--text-dim);
    font-size: 1rem;
}

/* ------------------------------------------------------------------- gates */

.gates {
    display: grid;
    grid-template-columns: 1fr;
    gap: calc(var(--step) * 2);
    width: 100%;
}

@media (min-width: 46rem) {
    .gates {
        grid-template-columns: 1fr 1fr;
    }
}

.gate {
    display: flex;
    flex-direction: column;
    gap: calc(var(--step) * 2);
    padding: calc(var(--step) * 3.5);
    border: var(--edge);
    border-radius: var(--radius);
    background: var(--surface);
    backdrop-filter: blur(8px) saturate(135%);
    -webkit-backdrop-filter: blur(8px) saturate(135%);
    text-decoration: none;
    color: inherit;
    transition: background 160ms ease, transform 160ms ease, border-color 160ms ease;
}

.gate:hover,
.gate:focus-visible {
    background: var(--surface-hover);
    border-color: var(--lift);
    transform: translateY(-2px);
}

.gate h2 {
    margin: 0;
    font-family: var(--font-display);
    font-size: 1.125rem;
    font-weight: 600;
    letter-spacing: -0.01em;
}

.gate p {
    margin: 0;
    flex: 1;
    color: var(--text-dim);
    font-size: 0.9375rem;
}

.gate .eyebrow {
    font-family: var(--font-data);
    font-size: 0.6875rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--text-faint);
}

/* The buttons - .action and every variant - are in the "controls" section
 * at the end of this file, with the theme switch and the person menu. */

/* -------------------------------------------------------------------- note */

.note {
    display: flex;
    gap: calc(var(--step) * 1.5);
    padding: calc(var(--step) * 1.75) calc(var(--step) * 2);
    border-radius: 10px;
    border: 1px solid var(--border);
    background: rgb(255 176 32 / 0.07);
    border-color: rgb(255 176 32 / 0.28);
    font-size: 0.875rem;
    color: var(--text-dim);
    line-height: 1.5;
}

.note::before {
    content: '';
    flex: none;
    width: 3px;
    border-radius: 2px;
    background: var(--caution);
}

/* A note is a sentence, so a block of it, with the bar drawn where the flex
 * row put it: as the row, every <strong> and link inside was a column of its
 * own - "The Director also approves a quote above | USD 5,000.00 | - another
 * currency ...", "Nothing matches "zzqq".  Show them all  ." (checkers, 28
 * October 2026). A phone and a popup already make every note a block, with
 * the bar as its left border (below); this is the same for the rest, and
 * :where keeps it as weak as .note alone, so the access grid's held note
 * stays held. */
@media (min-width: 40.0625rem) {
    .note:not(:where(.modal *, dialog *)) {
        display: block;
        position: relative;
        padding-left: calc(var(--step) * 3.5 + 3px);
    }

    .note:not(:where(.modal *, dialog *))::before {
        position: absolute;
        left: calc(var(--step) * 2);
        top: calc(var(--step) * 1.75);
        bottom: calc(var(--step) * 1.75);
    }
}

/* The portal standing open because sign-in is not built yet. Louder than
 * a caution on purpose: it is a state nobody should get used to. */
.note.warn {
    background: rgb(255 90 78 / 0.09);
    border-color: rgb(255 90 78 / 0.35);
    color: var(--text);
}

.note.warn::before { background: var(--alarm); width: 4px; }

.note code {
    font-family: var(--font-data);
    font-size: 0.9em;
    padding: 0.1em 0.35em;
    border-radius: 4px;
    background: rgb(255 255 255 / 0.08);
}

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

.data {
    font-family: var(--font-data);
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
}

.strip {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: calc(var(--step) * 1) calc(var(--step) * 3);
    font-family: var(--font-data);
    font-variant-numeric: tabular-nums;
    font-size: 0.6875rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--text-faint);
    text-align: center;
}

.strip span { white-space: nowrap; }


/* ------------------------------------------------------------------- staff */

.bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: calc(var(--step) * 2);
    padding: calc(var(--step) * 1.4) calc(var(--step) * 2);
    border: var(--edge);
    border-radius: var(--radius);
    background: var(--surface);
    backdrop-filter: blur(8px) saturate(135%);
    -webkit-backdrop-filter: blur(8px) saturate(135%);
}

.who {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.who strong {
    font-family: var(--font-display);
    font-weight: 600;
    letter-spacing: -0.01em;
}

.who span {
    font-family: var(--font-data);
    font-size: 0.75rem;
    color: var(--text-faint);
}

/* A sentence that explains - a state put in words, a reason somebody
 * typed, "every sign-in, change and deletion" - in the body font, wherever
 * it stands: beside a bar's title in .who, under a name in a cell, on a
 * line of its own. .data and the span under a bar's title are the figures'
 * small monospace, right for a count, a code or a date, and a whole
 * sentence in it reads as code (Hussein's rules: explanatory sentences in the body
 * font, not the monospace). A colour or a size written on the element
 * itself still wins. */
.hint,
.who .hint {
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    font-variant-numeric: normal;
    letter-spacing: normal;
}

.modules {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: calc(var(--step) * 2);
    width: 100%;
}

.module {
    display: flex;
    flex-direction: column;
    gap: calc(var(--step) * 1.25);
    padding: calc(var(--step) * 3);
    border: var(--edge);
    border-radius: var(--radius);
    background: var(--surface);
    backdrop-filter: blur(8px) saturate(135%);
    -webkit-backdrop-filter: blur(8px) saturate(135%);
    text-decoration: none;
    color: inherit;
    transition: background 160ms ease, transform 160ms ease, border-color 160ms ease;
}

.module:hover,
.module:focus-visible {
    background: var(--surface-hover);
    border-color: var(--lift);
    transform: translateY(-2px);
}

.module[aria-disabled='true'] {
    opacity: 0.5;
    pointer-events: none;
}

.module h3 {
    margin: 0;
    font-family: var(--font-display);
    font-size: 1.0625rem;
    font-weight: 600;
}

.module p {
    margin: 0;
    font-size: 0.875rem;
    color: var(--text-dim);
}

.role {
    align-self: flex-start;
    padding: 2px calc(var(--step));
    border-radius: 5px;
    background: rgb(46 58 143 / 0.28);
    color: var(--text-dim);
    font-family: var(--font-data);
    font-size: 0.625rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

/* ------------------------------------------------------------------ denied
 *
 * Deliberately without glass, gradient or motion. The plainness is the
 * message: this is not the normal path.
 */

body.denied {
    background-image: none;
    background: var(--bg-deep);
    display: grid;
    place-items: center;
    padding: calc(var(--step) * 3);
}

.refusal {
    max-width: 30rem;
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: calc(var(--step) * 2.5);
}

.refusal hr {
    width: 44px;
    height: 3px;
    border: 0;
    border-radius: 2px;
    background: var(--caution);
    margin: 0;
}

.refusal h1 {
    margin: 0;
    font-family: var(--font-display);
    font-size: 1.375rem;
    font-weight: 600;
}

.refusal p {
    margin: 0;
    color: var(--text-dim);
}

.refusal .data {
    font-size: 0.6875rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--text-faint);
}

/* --------------------------------------------------------------- footer */

/* Off on every screen (Hussein, 3 October 2026: "remove it from all
 * screens, i don't want to see it"). The company name and city under every
 * page said nothing a member of staff did not already know, and the counts
 * beside them repeat what each screen already shows above. Hidden here, once,
 * rather than edited out of two dozen pages; the printed quotation and tax
 * invoice keep their address, which a tax document must carry. */
.shell > footer {
    display: none;
}

/* ------------------------------------------------------------- foundations */

:focus-visible {
    outline: 2px solid var(--violet);
    outline-offset: 3px;
    border-radius: 4px;
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
    }

    .field .arc {
        stroke-dasharray: none;
    }
}

@media (max-width: 30rem) {
    .shell { padding: calc(var(--step) * 2); }
    .gate { padding: calc(var(--step) * 2.5); }
    .strip { gap: calc(var(--step) * 0.75) calc(var(--step) * 2); }
}

/* ---------------------------------------------------------------- settings */

/* Settings screens are tables of figures with a form on each row. They share
 * the shell's surfaces so a screen two levels in still looks like the front
 * door, but they need controls the front door has no use for. */

.card {
    display: flex;
    flex-direction: column;
    gap: calc(var(--step) * 1.5);
    padding: calc(var(--step) * 2.25);
    border: var(--edge);
    border-radius: var(--radius);
    background: var(--surface);
    backdrop-filter: blur(8px) saturate(135%);
    -webkit-backdrop-filter: blur(8px) saturate(135%);
}

.card h3 {
    margin: 0;
    font-family: var(--font-display);
    font-size: 1.0625rem;
    letter-spacing: -0.01em;
}

.card label {
    font-size: 0.8125rem;
    color: var(--text-dim);
}

.card summary {
    cursor: pointer;
    font-family: var(--font-display);
    font-size: 0.9375rem;
}

.card[open] summary { margin-bottom: calc(var(--step) * 1.5); }

.card input,
.rates input {
    min-height: 36px;
    padding: 0 calc(var(--step) * 1.25);
    border: 1px solid var(--border);
    border-radius: 7px;
    background: var(--bg-deep);
    color: var(--text);
    font-family: var(--font-body);
    font-size: 0.9375rem;
}

.card input { width: 100%; }

.card input:focus-visible,
.rates input:focus-visible { border-color: var(--violet); outline-offset: 1px; }

.card input:disabled,
.rates input:disabled { opacity: 0.45; cursor: not-allowed; }

table.rates {
    width: 100%;
    border-collapse: collapse;
    border: var(--edge);
    border-radius: var(--radius);
    background: var(--surface);

    /* clip trims the rows to the rounded corners as hidden did, without
     * making the table a scroll box of its own - which a heading would
     * then stick to instead of the window, and so never stick (see
     * "headings that stay put"). hidden first, for a browser that does not
     * know clip. */
    overflow: hidden;
    overflow: clip;
}

/* Tighter than the body text around it, because a table is scanned rather
 * than read. At the body's 16px and 1.55 line-height a row could not be
 * shorter than about 43px before anything was in it, and the flight list -
 * ten columns wide - spent that on every row while the date wrapped onto a
 * second line and made it 68px. Fewer rows on the screen is the whole cost,
 * and there is nothing gained for it. */
table.rates th,
table.rates td {
    padding: calc(var(--step) * 0.75) calc(var(--step) * 1.6);
    font-size: 0.875rem;
    line-height: 1.4;
    text-align: left;
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
}

/* A date, a serial, a reference: one line each. These are the columns that
 * were wrapping - "23 Aug 2026" broken after the month doubles the height of
 * the row it is on, and of every row beside it. The table already scrolls
 * sideways inside .table-scroll when it needs to. */
table.rates td.data { white-space: nowrap; }

table.rates tbody tr:last-child td { border-bottom: none; }

table.rates th {
    font-family: var(--font-display);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-faint);
}

/* Figures right-aligned with tabular figures, so a slipped decimal point is
 * visible as a misaligned column rather than having to be read for. */
table.rates .num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

table.rates tr.inactive td { opacity: 0.55; }

.inline-rate {
    display: flex;
    align-items: center;
    gap: var(--step);
    flex-wrap: wrap;
}

.inline-rate input[name='rate'] {
    font-family: var(--font-data);
    font-variant-numeric: tabular-nums;
    text-align: right;
}

/* Between a phone and a laptop the table stays a table and scrolls inside
 * its own box rather than pushing the page sideways.
 *
 * Only a table that has such a box - a .table-scroll, or a popup, which
 * scrolls itself. One standing straight on the page or in a card was made
 * 40rem wide as well, with nothing to scroll it in, and the page does not
 * slide sideways (body, overflow-x: clip), so between 641 and 720 pixels
 * its last columns were cut off at the window's edge: Passengers' lists,
 * the Calendar's folded list (review, 28 October 2026). Those lists now
 * have a box of their own; this keeps any list drawn later without one
 * as wide as the window, its columns closer, rather than cut off. :where
 * weighs nothing, so this rule is exactly as strong as it was. */
@media (max-width: 720px) {
    .table-scroll { overflow-x: auto; }
    :where(.table-scroll, dialog, .modal) table.rates { min-width: 40rem; }
}

/* On a phone it stops being a table.
 *
 * Scrolling sideways is what this used to do at every width below 720px, and
 * it is honest but useless: seven columns is 995 pixels on a screen 343
 * across, so you read two of them and swipe for the rest, once per row. A
 * flight record read that way is not read.
 *
 * So each row becomes a card: the first cell as its heading, the rest as
 * label and value on a line each. The labels come from the header row, put
 * onto the cells by portal.js, because the header itself is off the screen
 * by the time the cards are being read.
 *
 * The 40rem here must stay below the 720px above, or the two fight over the
 * same widths and the table is a stack that also insists on being 640 wide.
 */
@media (max-width: 40rem) {
    /* The map is atmosphere, and on a phone it stops being atmosphere.
     *
     * It is a fixed full-screen layer behind translucent cards. On a monitor
     * the cards are wide and the country is spread thin behind them; at 375
     * pixels the whole of Tanzania is drawn behind one column of cards, so a
     * neon border runs straight through the middle of a rate and an airfield
     * sits on top of a registration. Kept, because the portal should still
     * look like itself, and taken down to where it is a texture rather than
     * something the eye keeps trying to read.
     *
     * Not on the front page. There the map IS the page - it is the first
     * thing anybody sees of this company - and dimming it to a texture left
     * a dark rectangle with two login boxes on it. Behind a payroll it is
     * decoration; behind the front door it is the point.
     *
     * Nor on the tab Print and WhatsApp open while the quotation saves
     * (quote-preparing.php). That tab is a phone's more than anybody's, and
     * the flying aeroplane is all it has to show - dimmed to a texture it
     * was back to the blank screen Hussein called ugly. */
    body:not(.front):not(.waiting) .field { opacity: 0.3; }

    /* A note is [accent bar][text] as a flex row, and every emphasised word
     * inside it is another flex item. On a wide screen that is invisible; at
     * 343 pixels the row squeezes each item to its minimum and "The pad has
     * gaps." comes out four characters wide in a column of its own, beside a
     * column of serials, beside the rest of the sentence.
     *
     * The comment at .note.prose.warn below already worked this out and said
     * it needed "the same block treatment as .note.prose" - and then only set
     * a colour. This is that treatment, applied to every note rather than to
     * the one that was noticed, and only where it matters.
     *
     * The bar becomes a real border rather than a flex child, because a flex
     * child in a block container has no height to be 3 pixels of. */
    .note {
        display: block;
        border-left: 3px solid var(--caution);
    }

    .note::before { display: none; }
    .note.warn { border-left-color: var(--alarm); }

    /* Nothing to scroll any more, and leaving it on clips the card shadows. */
    .table-scroll { overflow-x: visible; }

    /* The card is the row. The table's own frame would draw a second box
     * around all of them.
     *
     * Not the access grid, and not a table marked "stays-table": a grid of
     * marks read down its columns - a tick or a cross under each heading -
     * is no use as a card of labelled lines, and a page that keeps one as a
     * grid on a phone makes it fit there itself (the Flights screen's
     * checklist, 22 October 2026). portal.js and table-sort.js leave the same
     * two alone. */
    :is(table.rates, table.compare):not(.access, .stays-table) {
        min-width: 0;
        border: none;
        background: none;
        overflow: visible;
    }

    :is(table.rates, table.compare):not(.access, .stays-table) thead { display: none; }

    :is(table.rates, table.compare):not(.access, .stays-table) tbody tr {
        display: block;
        margin-bottom: calc(var(--step) * 1.5);
        padding: calc(var(--step) * 1.75) calc(var(--step) * 2);
        border: var(--edge);
        border-radius: var(--radius);

        /* Solid where the map is behind it: "solid cards on a phone", by
         * the laptop's solid lists further down. */
        background: var(--surface);
    }

    :is(table.rates, table.compare):not(.access, .stays-table) tbody td {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: var(--step);
        flex-wrap: wrap;
        padding: 0.3rem 0;
        border-bottom: none;
        text-align: right;

        /* The table rules set nowrap on every cell, which is right in a table -
         * a cost centre broken over two lines is harder to scan down a
         * column. In a card there is no column, and nowrap instead makes
         * the card 451 pixels wide inside a 343 pixel screen, so half of
         * every employee's pay runs off the right-hand edge. */
        white-space: normal;
        min-width: 0;
    }

    /* A value too long for its label's line goes onto the next one on the
     * right, where the values are: space-between put a line's only item at
     * its start, so a file's long subject - "RE: TEST - REQUEST FOR AIRSIDE
     * PASSES" - sat at the left under its label while the next card's sat on
     * the right (checkers, 28 October 2026). Labelled cells only; the action
     * buttons' cell keeps its spread, and a paper's cell its own (td.doc). */
    :is(table.rates, table.compare):not(.access, .stays-table) tbody td[data-label]:not([data-label=''], .doc) {
        justify-content: flex-end;
    }

    :is(table.rates, table.compare):not(.access, .stays-table) tbody td::before {
        content: attr(data-label);
        flex: 0 0 auto;
        text-align: left;

        /* Pushes everything after it to the right, however many things there
         * are. space-between on its own only works for a cell holding one
         * value: give it two - a figure and the note beside it - and it
         * spreads all three evenly, which puts the value in the middle of the
         * line looking like it belongs to neither side. */
        margin-right: auto;
        font-family: var(--font-display);
        font-size: 0.7rem;
        font-weight: 600;
        letter-spacing: 0.08em;
        text-transform: uppercase;
        color: var(--text-faint);
    }

    /* The first cell is what the card is about - a serial, a name - so it
     * reads as the heading rather than as another labelled value. */
    :is(table.rates, table.compare):not(.access, .stays-table) tbody td:first-child {
        display: block;
        text-align: left;
        font-size: 1.05rem;
        font-weight: 600;
        padding-bottom: 0.6rem;
        margin-bottom: 0.4rem;
        border-bottom: 1px solid var(--border);
    }

    /* Except a cell across the whole row that opens out into a list of its
     * own - the change log's What changed. Its Field, Before and After are
     * cards of their own, each headed by the field; drawn as this card's
     * heading, every line of them was large and bold under a rule (review,
     * 28 October 2026). Nor the one cell of a "nothing here yet" row - No
     * letter numbers on this file yet, Nothing is on its way in: drawn as a
     * heading it was a card with a rule under it and nothing below the rule
     * (checkers, 28 October 2026). */
    :is(table.rates, table.compare):not(.access, .stays-table) tbody td[colspan]:first-child:is(:only-child, :has(> table)) {
        font-size: inherit;
        font-weight: inherit;
        padding-bottom: 0;
        margin-bottom: 0;
        border-bottom: 0;
    }

    /* Cells with nothing to label: the action buttons, and the "nothing here
     * yet" line that spans the row. */
    :is(table.rates, table.compare):not(.access, .stays-table) tbody td:first-child::before,
    :is(table.rates, table.compare):not(.access, .stays-table) tbody td[data-label='']::before,
    :is(table.rates, table.compare):not(.access, .stays-table) tbody td:not([data-label])::before {
        content: none;
    }

    :is(table.rates, table.compare):not(.access, .stays-table) tbody td.doc {
        justify-content: flex-start;
        flex-wrap: wrap;
        padding-top: 0.7rem;
    }

    /* An empty cell would otherwise draw its label and nothing beside it. */
    :is(table.rates, table.compare):not(.access, .stays-table) tbody td:empty { display: none; }

    /* The whole card is the target, so the row link does not need to be a
     * small piece of text in the corner of it. */
    .rates tbody tr.go .rowgo { display: inline-block; }
}

/* A card's word-sized action under a finger - Fuel Suppliers' retire,
 * System's Change - has its thumb-sized box (the .linkish rule, "a thumb is
 * not a cursor") inside its own line of the card. Its line was 36 tall, so
 * the box reached 7 pixels down over the line under it, and a tap at the top
 * of that line, which used to open the supplier, retired it with no question
 * asked (checkers, 28 October 2026). The line is the box's 44 now, the word
 * in its middle. */
@media (max-width: 40rem) and (pointer: coarse) {
    :is(table.rates, table.compare):not(.access, .stays-table) tbody td.doc:has(.linkish) {
        align-items: center;
        min-height: 44px;
        padding-block: 0;
    }
}


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

/* Each flight has three slots and the useful glance is "which one is still
 * missing?" - so filed, empty and not-applicable have to be told apart from
 * across the room, not read one by one. */

.slot {
    display: inline-flex;
    align-items: center;
    gap: calc(var(--step) * 0.75);
    min-height: 30px;
    padding: 0 calc(var(--step) * 1.25);
    border-radius: 7px;
    font-size: 0.8125rem;
    white-space: nowrap;
}

.slot.filed {
    background: rgb(108 92 231 / 0.12);
    border: 1px solid rgb(108 92 231 / 0.3);
}

.slot.filed a { color: var(--text); text-decoration: none; font-weight: 500; }
.slot.filed a:hover { text-decoration: underline; }
.slot.filed .data { color: var(--text-faint); font-size: 0.6875rem; }

/* Some of the manifest read and some did not - "2 of 3". Marked, because it
 * is a job somebody still has to do: open the scan and type the name the
 * reader could not make out. Amber rather than red; the flight is recorded,
 * it is just not finished. */
.slot.part {
    background: rgb(224 168 46 / 0.14);
    border: 1px solid rgb(224 168 46 / 0.4);
}

.slot.part a { color: var(--text); text-decoration: none; font-weight: 500; }
.slot.part a:hover { text-decoration: underline; }

/* Deliberately faint. It is not a gap in the paperwork - there was never a
 * manifest to file - and it should not read like one. */
.slot.none {
    color: var(--text-faint);
    font-style: italic;
    opacity: 0.6;
}

/* The whole label is the target, so it is a comfortable tap on a phone
 * rather than a hunt for a small button. */
.attach-label {
    cursor: pointer;
    border: 1px dashed var(--border);
    color: var(--text-dim);
    transition: border-color 140ms ease, color 140ms ease;
}

.attach-label:hover { border-color: var(--lift); color: var(--text); }
.attach-label input[type='file'] { display: none; }

/* A destructive control that should not look like a button worth pressing. */
.linkish {
    background: none;
    border: 0;
    padding: 0 calc(var(--step) * 0.75);
    color: var(--text-faint);
    cursor: pointer;
    font-size: 1rem;
    line-height: 1;
}

.linkish:hover { color: var(--alarm); }

table.flights td { vertical-align: middle; }

/* A checkbox and its words, on one line, both clickable. */
.check {
    display: flex !important;
    flex-direction: row !important;
    align-items: center;
    gap: calc(var(--step) * 1.25);
    cursor: pointer;
}

.check input[type='checkbox'] {
    width: 18px;
    min-height: 18px;
    height: 18px;
    flex: none;
    accent-color: var(--violet);
    cursor: pointer;
}

.check span { font-size: 0.875rem; color: var(--text-dim); }
.check:hover span { color: var(--text); }

/* ------------------------------------------------------------------ icons */

.icon {
    flex: none;
    /* Sits on the text baseline rather than the line box, which is what
     * stops an icon beside a word looking like it is falling over. */
    vertical-align: -0.18em;
}

/* A tick reads at a glance down a column; the word "yes" has to be read. */
.mark-yes { color: #3ddc84; }
.mark-no  { color: var(--text-faint); opacity: 0.55; }

/* Present for screen readers, invisible to everyone else - a tick alone
 * tells somebody using a reader nothing at all. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* Column headings carry their icon too, so the tick below has something to
 * belong to. */
th .icon { vertical-align: -0.2em; margin-right: 0.4em; opacity: 0.7; }


/* Yes/no columns centre so the ticks form a readable line down the table.
 * Left-aligned they wander with the heading width and the glance is lost. */
table.rates th.mid,
table.rates td.mid { text-align: center; }

table.rates th.mid .icon { margin-right: 0.35em; }

/* A badge that is a problem rather than a label.
 *
 * .role is the neutral chip - "Ready", "Built in", "Commercial". Some of
 * what it was carrying is not a label at all but an outstanding job:
 * "Needs review", "Unserviceable", "No position". Those should read as
 * something to act on from across the room, not as another grey tag among
 * the grey tags. */
.role.flag {
    background: rgb(255 90 78 / 0.16);
    border: 1px solid rgb(255 90 78 / 0.4);
    color: #ff8578;
    font-weight: 600;
}

:root[data-theme='light'] .role.flag { color: #c0392b; }

@media (prefers-color-scheme: light) {
    :root:not([data-theme]) .role.flag { color: #c0392b; }
}

/* --------------------------------------------------------------- the map */

.airport-map {
    height: 30rem;
    border-radius: var(--radius);
    border: var(--edge);
    overflow: hidden;
    /* Sits under the theme buttons and any popup the map opens. */
    z-index: 0;
}

/* OpenStreetMap tiles are drawn for paper-white pages. Left alone they glare
 * out of a dark interface and pull the eye off the markers, which are the
 * only part carrying information. Dimmed and cooled, they read as ground. */
:root:not([data-theme='light']) .airport-map .leaflet-tile-pane {
    filter: invert(1) hue-rotate(180deg) brightness(0.94) contrast(1.06) saturate(0.7);
}

@media (prefers-color-scheme: light) {
    :root:not([data-theme]) .airport-map .leaflet-tile-pane { filter: none; }
}

:root[data-theme='light'] .airport-map .leaflet-tile-pane { filter: none; }

.airport-map .leaflet-container {
    background: var(--bg-deep);
    font-family: var(--font-body);
}

.airport-map .leaflet-popup-content-wrapper,
.airport-map .leaflet-popup-tip {
    background: var(--bg-deep);
    color: var(--text);
    border: 1px solid var(--border);
}

.airport-map .leaflet-popup-content { font-size: 0.8125rem; line-height: 1.6; }
.airport-map .leaflet-popup-content a { color: var(--violet); }
.airport-map .leaflet-popup-content .codes {
    font-family: var(--font-data);
    font-size: 0.6875rem;
    color: var(--text-faint);
}
.airport-map .leaflet-popup-content .flagged { color: #ff8578; font-weight: 600; }

.airport-map .leaflet-control-attribution {
    background: rgb(0 0 0 / 0.5);
    color: var(--text-faint);
}

.airport-map .leaflet-control-attribution a { color: var(--text-dim); }

.map-failed { padding: calc(var(--step) * 3); color: var(--alarm); }

/* The key. Without it the colours are decoration rather than information. */
.map-key {
    display: flex;
    flex-wrap: wrap;
    gap: calc(var(--step) * 2);
    font-size: 0.75rem;
    color: var(--text-dim);
}

.map-key span { display: inline-flex; align-items: center; gap: calc(var(--step) * 0.75); }

.map-key .dot {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    display: inline-block;
}

.map-key .dot.verified { background: #3ddc84; }
.map-key .dot.charted  { background: var(--violet); }
.map-key .dot.flagged  { background: var(--alarm); }

/* ------------------------------------------------------------------ tabs */

/* Three views of one thing. Deliberately not buttons: they are links with
 * their own address, so a particular view can be sent to somebody and the
 * back button behaves the way anybody expects it to. */

.tabs {
    display: flex;
    flex-wrap: wrap;
    gap: calc(var(--step) * 0.5);
    border-bottom: 1px solid var(--border);
    margin-bottom: calc(var(--step) * 0.5);
}

.tab {
    display: inline-flex;
    align-items: center;
    gap: calc(var(--step) * 1.25);
    padding: calc(var(--step) * 1.75) calc(var(--step) * 2.5);
    border: 1px solid transparent;
    border-bottom: none;
    border-radius: 10px 10px 0 0;
    color: var(--text-dim);
    text-decoration: none;
    font-family: var(--font-display);
    font-size: 0.9375rem;
    /* The tab sits on the rule rather than above it. */
    margin-bottom: -1px;
    transition: color 140ms ease, background 140ms ease;
}

.tab:hover { color: var(--text); background: var(--surface); }

.tab.on {
    color: var(--text);
    background: var(--surface);
    border-color: var(--border);
    /* Erasing the rule under the active tab is what joins it to the panel
     * below - without it the tab floats and reads as another button. */
    border-bottom: 1px solid var(--bg);
    font-weight: 600;
}

/* The count is the whole reason the queries are safe to hide behind a tab. */
.tab .count {
    min-width: 1.35rem;
    padding: 0 calc(var(--step) * 0.75);
    border-radius: 999px;
    background: var(--surface-hover);
    color: var(--text-dim);
    font-family: var(--font-data);
    font-size: 0.75rem;
    text-align: center;
    line-height: 1.35rem;
}

.tab.has-work .count {
    background: rgb(255 90 78 / 0.16);
    color: #ff8578;
}

.tab.on.has-work .count { background: var(--alarm); color: #ffffff; }

/* ------------------------------------------------- the map, full screen */

/* The fallback, for browsers that will not give up the whole display - iOS
 * Safari offers the Fullscreen API for video and nothing else. Pinned over
 * the page rather than owning the screen, which to anybody using it looks
 * the same. */
.airport-map.expanded {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    /* Above the shell and its sticky headers, below nothing. */
    z-index: 2000;
    border-radius: 0;
    border: none;
}

/* The real thing. The browser paints its own black behind a fullscreen
 * element, and a map floating on black with rounded corners looks like a
 * mistake. */
.airport-map:fullscreen {
    border-radius: 0;
    border: none;
}

.airport-map:fullscreen .leaflet-container,
.airport-map.expanded .leaflet-container {
    height: 100%;
}

/* Leaflet's own control styling is built for a white page. */
.airport-map .leaflet-bar a {
    background: var(--bg-deep);
    color: var(--text-dim);
    border-bottom-color: var(--border);
}

.airport-map .leaflet-bar a:hover { background: var(--surface-hover); color: var(--text); }

.airport-map .map-full a {
    display: flex;
    align-items: center;
    justify-content: center;
}

/* ------------------------------------------------- mark, larger */

/* The mark was 34px, which read as a favicon rather than as the company.
 * It is the only piece of Tanzanair's own identity on the page. */
.mark img { height: 46px; }

@media (max-width: 640px) {
    .mark img { height: 38px; }
}

/* ------------------------------------------------------- the pinned row
 *
 * One row per screen stays at the very top of the window as the list
 * scrolls, and nothing else does except the list's own column headings,
 * which stop directly under it (Hussein, 22 October 2026: "when scrolling
 * the headings should be frozen, and i don't like the way the tab section
 * floats in between while scrolling lists, move it to top so there is max
 * space in the middle").
 *
 * Until then every bar on a screen and every tab line was sticky. The bar
 * stuck at the top of the window and the tabs at 4.6rem, a guess at the
 * height of the bar above them - and on the Flights screen, which has no bar,
 * the tabs, search and buttons hung 74 pixels down the window with rows
 * showing above them, and on a phone they covered half the screen. A screen
 * with two bars had both pinned on top of each other, and the column
 * headings, pinned at the top of the window too, were hidden under them.
 *
 * So the screen chooses its one row by giving it the class "pin": its tab
 * strip if it has one, otherwise its first bar. A tile page pins nothing.
 * Search boxes, filters and the screen's name scroll away with the page.
 * portal.js measures the pinned row into --pin-h, which is where the column
 * headings stop; it is nought where nothing is pinned.
 *
 * Solid, not see-through. The moving map behind the page showed through the
 * old near-opaque bar, and rows sliding under a row that lets them show
 * read as two things on top of each other. */
:root { --pin-h: 0px; }

:root, :root[data-theme='dark'] { --pin-bg: #0d112e; }
:root[data-theme='light'] { --pin-bg: #f6f7fb; }

@media (prefers-color-scheme: light) {
    :root:not([data-theme]) { --pin-bg: #f6f7fb; }
}

.middle > .pin {
    position: sticky;
    top: 0;
    z-index: 20;
    background: var(--pin-bg);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;

    /* The width of the column, whatever the column does with what is in it.
     * Charter Rates' column centres its parts (.middle, not .middle wide),
     * so its held bar was a 485-pixel box in the middle, and the rows going
     * under it showed on both sides, beside headings held the whole width
     * (checkers, 28 October 2026). */
    align-self: stretch;
}

/* Held at the top of the window, a bar's rounded corners were four little
 * windows onto the rows going under it; square while held (portal.js says
 * when), round again at rest. */
.middle > .bar.pin.stuck { border-radius: 0; }

/* A bar that has wrapped onto a second line lets go (portal.js says when):
 * a sixth of a laptop screen held for a search box was not worth it. */
.middle > .bar.pin.unpinned { position: static; }

/* A link or a field reached with the keyboard is scrolled clear of the
 * pinned row and the headings held under it, rather than left underneath
 * them where its focus ring cannot be seen - as a quotation already does
 * for its own held header (quote.css, which wins: this weighs nothing). */
:where(html:has(.middle > .pin, table.held)) { scroll-padding-top: calc(var(--pin-h) + 3.5rem); }

/* A pinned row over a map is a row covering the map. */
.airport-map.expanded ~ .pin,
.airport-map:fullscreen ~ .pin { position: static; }

/* The tab strip, pinned, is always one row. Seven tabs on a phone wrapped
 * onto five lines; now the row scrolls sideways under a finger instead, with
 * the tab you are on brought into view (portal.js), and the screen's main
 * action - File a flight, Add someone - held at its right-hand end in
 * .pin-do whatever the tabs are scrolled to. The Add has lived at the tabs'
 * right-hand end since 3 October 2026 (Hussein); the search and Back that
 * shared the line with it moved to .list-tools below, because a strip that
 * held them was two rows on a laptop and five on a phone. Lifted off the
 * rule, so the button sits beside the tabs and not on the line the active
 * tab joins. */
.tabs.pin {
    flex-wrap: nowrap;
    align-items: flex-end;
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    margin-bottom: 0;

    /* The rule the tabs stand on, drawn inside the strip rather than as its
     * border: a box that scrolls clips what hangs over its edge, and the
     * tab you are on used to hang a pixel over the border to rub it out. */
    border-bottom: 0;
    box-shadow: inset 0 -1px 0 var(--border);
}

.tabs.pin::-webkit-scrollbar { display: none; }

/* Seven tabs and File a flight fit one row on a 1280 laptop at these sizes;
 * at the tabs' own 15px and 20px a side they needed 1400 pixels. */
.tabs.pin .tab {
    flex: none;
    margin-bottom: 0;
    padding: 11px 12px;
    gap: 8px;
    font-size: 0.875rem;
}

.tabs.pin .tab.on { border-bottom-color: var(--pin-bg); }

.tabs.pin .pin-do {
    position: sticky;
    right: 0;
    display: flex;
    align-items: center;
    gap: var(--step);
    flex: none;
    margin-left: auto;
    padding: 0 0 5px calc(var(--step) * 1.5);
    background: var(--pin-bg);
    box-shadow: inset 0 -1px 0 var(--border);
}

/* Where the tabs run on under the button, they fade into it rather than
 * being cut off by it - the sign that the strip goes on. */
.tabs.pin .pin-do::before {
    content: '';
    position: absolute;
    top: 0;
    right: 100%;
    bottom: 1px;
    width: 28px;
    background: linear-gradient(to right, transparent, var(--pin-bg));
    pointer-events: none;
}

.tabs.pin .pin-do .action { white-space: nowrap; }

/* A strip whose action has a box for live search to draw it again in
 * (src/View/consignments/tabs.php) keeps the box when this person may not
 * use the action - and an empty box is no solid end to fade into. */
.tabs.pin .pin-do:empty { display: none; }

/* Focus on a tab scrolled half out of the strip is brought into view by the
 * browser; the ring is drawn inside the tab so the strip's edge does not cut
 * it off. */
.tabs.pin .tab:focus-visible,
.tabs.pin .pin-do .action:focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: -2px;
}

/* With a mouse there is no finger to swipe the strip along, so a strip too
 * long for the window - seven tabs on a 1024 screen - shows a slim
 * scrollbar under its rule. Only when it is too long: a strip that fits has
 * nothing to scroll and draws none. */
@media (hover: hover) and (pointer: fine) {
    .tabs.pin {
        scrollbar-width: thin;
        scrollbar-color: var(--border) transparent;
    }
}

/* The row of tools under a pinned tab strip: the search, the filters and
 * Back. It scrolls away with the page; the strip above it does not. */
.list-tools {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--step);
}

.list-tools .bar-search {
    flex: 1 1 16rem;
    max-width: 30rem;
    margin: 0 auto 0 0;
}

.list-tools .action { flex: none; white-space: nowrap; }

/* Where a tab has no search - Cancelled - the row says how many it holds in
 * the search's place, and Back keeps to the right. */
.list-tools .list-count {
    flex: 1 1 auto;
    margin: 0;
    color: var(--text-dim);
}

.list-tools .list-count strong { color: var(--text); }

@media (max-width: 40rem) {
    /* A finger's worth of tab and no more: the strip is the only thing held
     * on a phone, so every pixel of it is a pixel of list lost.
     *
     * Words and counts, no icons, and the tab you are on underlined rather
     * than drawn as a folder tab (Hussein, 22 October 2026, of the Flights
     * screen on a phone: "its very ugly"). The folder tab was a raised box
     * that joined the page under it; on a strip that slides sideways it was
     * a box sliding along with the rest, and read as one more button. The
     * icons took a fifth of each tab, so fewer tabs were in view. */
    .tabs.pin .tab {
        min-height: 44px;
        padding: 0 11px;
        gap: 6px;
        border: 0;
        border-radius: 0;
        background: none;
    }

    .tabs.pin .tab > svg { display: none; }

    .tabs.pin .tab.on {
        color: var(--text);
        box-shadow: inset 0 -2px 0 var(--violet);
    }

    /* The main action keeps its icon and gives up its word, which is still
     * read out (the same hiding as .topbar-home > span). */
    .tabs.pin .pin-do .action > span {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        clip-path: inset(50%);
        white-space: nowrap;
    }

    /* As tall as the tabs beside it, and still a finger wide. */
    .tabs.pin .pin-do .action {
        min-height: 38px;
        padding-inline: 11px;
    }

    .tabs.pin .pin-do { padding-bottom: 3px; }

    /* One line: the search takes what is left beside Filters and Back, and
     * Back is its arrow alone, its word still read out. The search had the
     * whole width, with Filters and Back on a line under it: two lines, 88
     * pixels, before the list began (22 October 2026). Where a tab has two
     * choices of its own - the Checklist's "Incomplete only" and "Not
     * costed yet" - they share the line under it, half each.
     *
     * Filters is its icon alone too, as Back is and as the Sort button
     * beside them is (below, "the same choice as a button"): with its word
     * and the Sort button the row came to 330 pixels at 320 and Back fell
     * onto a line of its own, and at 375 the search was cut to "Search
     * brief" (review, 22 October 2026). Its dot still says when a filter is
     * on, in the corner.
     *
     * Filters, Sort and Back are a finger square, 44 by 44 - they were 44
     * wide but only a button's 36 tall (review, 22 October 2026) - and the
     * search beside them is as tall, so the row is one even line. Taller,
     * not wider, so it is still one line at 320. */
    .list-tools { flex-wrap: wrap; justify-content: flex-start; gap: 8px; }
    .list-tools .bar-search { order: 0; flex: 1 1 8rem; max-width: none; margin: 0; }
    .list-tools .bar-search input[type='search'] { min-height: 44px; }

    .list-tools .list-back,
    .list-tools .list-filters { width: 44px; min-height: 44px; padding: 0; justify-content: center; }

    .list-tools .list-back { order: 1; }

    .list-tools .list-back > svg,
    .list-tools .list-filters > svg { margin: 0; }

    .list-tools .list-filters.is-set::after {
        position: absolute;
        top: 7px;
        right: 7px;
        margin: 0;
    }

    .list-tools .list-filters > span,
    .list-tools .list-back > span {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .list-tools .list-toggle {
        order: 2;
        flex: 1 1 calc(50% - 4px);
        min-width: 0;
        padding-inline: 10px;
        line-height: 1.2;
        white-space: normal;
    }
}

/* Under a finger the strip's main action is a finger square, 44 by 44, as
 * tall as the tabs beside it really are. The 38 above weighs more than the
 * touch screen's 44 for every button ("a thumb is not a cursor"), so Take a
 * letter number, Add someone and the Consignments + were 38 by 38, and
 * Bookings' Put in maintenance 38 wide (checkers, 28 October 2026). It
 * stands on the strip's rule rather than 3 pixels above it, so the strip
 * stays the tabs' 44 tall. */
@media (max-width: 40rem) and (pointer: coarse) {
    .tabs.pin .pin-do .action {
        min-width: 44px;
        min-height: 44px;
    }

    .tabs.pin .pin-do { padding-bottom: 0; }
}

/* A bar on a phone wraps to two, three, five lines, and the list under it
 * is cards with no headings to hold - pinning it bought nothing and cost up
 * to a third of the screen. On a phone a bar scrolls away; so it does on a
 * phone on its side, wider than 40rem but under 32rem tall, where even one
 * line of bar is a sixth of the screen (the quotation's bar lets go on the
 * same two, quote.css). A tab strip is one row, and stays. */
@media (max-width: 40rem), (max-height: 32rem) {
    .middle > .bar.pin { position: static; }
}

/* --------------------------------------------------------- query groups */

/* Grouped by the question being asked, and compact. The old version gave
 * every query a full card with a paragraph in it, so twelve of them filled
 * three screens and reading the list was itself the work. */

.qgroup {
    border: var(--edge);
    border-radius: var(--radius);
    background: var(--surface);
    overflow: clip;
}

.qgroup-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: calc(var(--step) * 1.5);
    padding: calc(var(--step) * 2) calc(var(--step) * 2.5);
    border-bottom: 1px solid var(--border);
    background: var(--surface-hover);
}

.qgroup-title {
    display: inline-flex;
    align-items: center;
    gap: calc(var(--step) * 1.25);
    font-family: var(--font-display);
    font-size: 0.9375rem;
    font-weight: 600;
    color: var(--text);
    white-space: nowrap;
}

/* What settling this kind involves, said once for the group rather than
 * repeated on every row - which is what made the rows tall. */
.qgroup-note {
    flex: 1 1 22rem;
    font-size: 0.8125rem;
    line-height: 1.5;
    color: var(--text-dim);
}

.qrow {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: calc(var(--step) * 1.5);
    padding: calc(var(--step) * 1.5) calc(var(--step) * 2.5);
    border-bottom: 1px solid var(--hairline, rgb(255 255 255 / 0.06));
}

.qrow:last-child { border-bottom: none; }
.qrow:hover { background: var(--surface-hover); }

.qwhat {
    flex: 0 0 14rem;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: calc(var(--step) * 1.25);
    font-size: 0.875rem;
}

.qwhat .qsub { color: var(--text-faint); font-size: 0.75rem; }

.qwhy {
    flex: 1 1 18rem;
    font-size: 0.8125rem;
    line-height: 1.45;
    color: var(--text-dim);
}

.qdo {
    display: flex;
    align-items: center;
    gap: calc(var(--step) * 0.75);
    flex-wrap: wrap;
    margin-left: auto;
}

.qdo form { display: inline-flex; }

.qfix { gap: calc(var(--step) * 0.75); align-items: center; }
.qfix input[type='number'] { width: 5.5rem; }
.qfix input[type='text'] { width: 11rem; }

.qfix input {
    min-height: 32px;
    padding: 0 calc(var(--step) * 1.25);
    border: 1px solid var(--border);
    border-radius: 7px;
    background: var(--bg-deep);
    color: var(--text);
    font-family: var(--font-body);
    font-size: 0.8125rem;
}

.qfix input:focus-visible { border-color: var(--violet); outline-offset: 1px; }


/* Shown rather than hidden when locked. A row with no controls reads as a
 * screen that cannot do anything. */
.action.ghost.locked { opacity: 0.6; }
.action.ghost.locked:hover { opacity: 1; }

/* The distance-change form, on one line.
 *
 * The inputs were sized in characters and the form wrapped, which in a
 * narrow table cell turned every row into three stacked boxes. */
.dfix {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: calc(var(--step) * 0.75);
}

.dfix input {
    min-height: 32px;
    padding: 0 calc(var(--step) * 1.25);
    border: 1px solid var(--border);
    border-radius: 7px;
    background: var(--bg-deep);
    color: var(--text);
    font-family: var(--font-body);
    font-size: 0.8125rem;
}

.dfix input[type='number'] { width: 5rem; flex: none; font-family: var(--font-data); text-align: right; }
.dfix input[type='text'] { width: 9rem; min-width: 6rem; }
.dfix input:focus-visible { border-color: var(--violet); outline-offset: 1px; }
.dfix .action { flex: none; }

/* -------------------------------------------------------------- the popup
 *
 * A dot on a map is worth clicking only if it tells you something. The old
 * one was a plain dark rectangle with three codes run together by dots -
 * ICAO, IATA and the internal code look alike, so nobody could tell which
 * was which, and the box read as a tooltip somebody forgot to design.
 *
 * Labelled cells and a lit edge instead. It is still the portal's colours;
 * it just stops looking like a hole cut in the map. */
.airport-map .leaflet-popup-content-wrapper {
    padding: 0;
    overflow: hidden;
    border: 1px solid var(--arc);
    border-radius: 13px;
    box-shadow: 0 14px 34px rgb(4 6 20 / 0.5), 0 0 0 4px rgb(108 92 231 / 0.1);
}

.airport-map .leaflet-popup-content { margin: 0; width: auto !important; }
.airport-map .leaflet-popup-tip { border-color: var(--arc); }

.apop { min-width: 12.5rem; }

/* The band across the top is what carries the decoration. The rest stays
 * quiet so the codes read as data rather than as more styling. */
.apop-head {
    padding: calc(var(--step) * 1.5) calc(var(--step) * 2);
    background:
        linear-gradient(135deg, rgb(108 92 231 / 0.34), rgb(46 58 143 / 0.16) 60%, transparent),
        var(--surface);
    border-bottom: 1px solid var(--border);
}

.apop-head strong {
    display: block;
    font-family: var(--font-display);
    font-size: 0.9375rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    line-height: 1.25;
    color: var(--text);
}

.apop-head .where {
    display: block;
    margin-top: 2px;
    font-size: 0.75rem;
    color: var(--text-dim);
}

.apop-flagged .apop-head {
    background:
        linear-gradient(135deg, rgb(255 90 78 / 0.3), rgb(255 90 78 / 0.08) 60%, transparent),
        var(--surface);
}

.apop-codes {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1px;
    background: var(--border);
    border-bottom: 1px solid var(--border);
}

.apop-codes .cell {
    padding: calc(var(--step) * 1.25) calc(var(--step) * 2);
    background: var(--bg-deep);
}

.apop-codes .label {
    display: block;
    font-size: 0.5625rem;
    font-weight: 700;
    letter-spacing: 0.11em;
    text-transform: uppercase;
    color: var(--text-faint);
}

.apop-codes .value {
    display: block;
    margin-top: 1px;
    font-family: var(--font-data);
    font-size: 0.875rem;
    letter-spacing: 0.03em;
    color: var(--text);
}

.apop-codes .value.none { color: var(--text-faint); }

.apop-flag {
    margin: 0;
    padding: calc(var(--step) * 1.25) calc(var(--step) * 2);
    font-size: 0.75rem;
    font-weight: 600;
    color: #ff8578;
    border-bottom: 1px solid var(--border);
}

.apop-do {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--step);
    padding: calc(var(--step) * 1.5) calc(var(--step) * 2);
}

.apop-lines {
    padding: calc(var(--step) * 0.75) calc(var(--step) * 1.75);
    border: 1px solid var(--arc);
    border-radius: 999px;
    background: rgb(108 92 231 / 0.18);
    color: var(--text);
    font-family: var(--font-body);
    font-size: 0.75rem;
    font-weight: 600;
    cursor: pointer;
}

.apop-lines:hover { background: rgb(108 92 231 / 0.34); }

.airport-map .leaflet-popup-content .apop-do a {
    font-size: 0.75rem;
    color: var(--text-dim);
    text-decoration: none;
    border-bottom: 1px dotted var(--text-faint);
}

.airport-map .leaflet-popup-content .apop-do a:hover { color: var(--text); }

/* The figure that appears when a line is hovered. Small, because it follows
 * the pointer across a map already full of dots. */
.airport-map .sector-tip {
    padding: calc(var(--step) * 0.75) calc(var(--step) * 1.25);
    background: var(--bg-deep);
    border: 1px solid var(--arc);
    border-radius: 8px;
    box-shadow: 0 6px 18px rgb(4 6 20 / 0.5);
    color: var(--text-dim);
    font-family: var(--font-data);
    font-size: 0.6875rem;
    line-height: 1.5;
    white-space: nowrap;
}

.airport-map .sector-tip strong { color: var(--text); font-size: 0.8125rem; }
.airport-map .sector-tip::before { display: none; }

/* The popup shadow is sized for a dark page. On white the same one reads as
 * a smudge under the box rather than the box standing off the map. */
:root[data-theme='light'] .airport-map .leaflet-popup-content-wrapper,
:root[data-theme='light'] .airport-map .sector-tip {
    box-shadow: 0 8px 22px rgb(25 34 98 / 0.18);
}

@media (prefers-color-scheme: light) {
    :root:not([data-theme='dark']) .airport-map .leaflet-popup-content-wrapper,
    :root:not([data-theme='dark']) .airport-map .sector-tip {
        box-shadow: 0 8px 22px rgb(25 34 98 / 0.18);
    }
}

/* ------------------------------------------------------------- tile detail
 *
 * The grid was five identical rectangles: the same border, the same glass,
 * the same three lines of text. Nothing to steer by, so every visit meant
 * reading every heading again. An icon per screen is learned in two or three
 * visits and after that the grid is navigated by shape.
 *
 * The motion is the second half of it. A tile that does not answer a hover
 * reads as a picture of a button rather than a button. */
.module { position: relative; overflow: hidden; }

.module-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--step);
}

.module-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    border: 1px solid var(--border);
    border-radius: 11px;
    background:
        linear-gradient(140deg, rgb(108 92 231 / 0.26), rgb(46 58 143 / 0.12) 65%, transparent),
        var(--surface);
    color: var(--text-dim);
    transition: color 200ms ease, border-color 200ms ease, transform 260ms cubic-bezier(.2, .8, .3, 1.2);
}

.module-mark .icon { width: 22px; height: 22px; }

/* The heading carries the chevron, so it costs no height of its own and
 * cannot land on the last line of a long description. */
.module h3 {
    display: flex;
    align-items: center;
    gap: calc(var(--step) * 0.75);
}

.module-go {
    display: inline-flex;
    flex: none;
    color: var(--text-faint);
    opacity: 0;
    transform: translateX(-5px);
    transition: opacity 200ms ease, transform 200ms ease, color 200ms ease;
}

/* A slow sheen across the tile on hover. Off the left edge at rest, so it
 * costs nothing until somebody points at it. */
.module::after {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: linear-gradient(
        105deg,
        transparent 35%,
        rgb(255 255 255 / 0.06) 50%,
        transparent 65%
    );
    transform: translateX(-100%);
    transition: transform 620ms ease;
}

.module:hover::after,
.module:focus-visible::after { transform: translateX(100%); }

.module:hover .module-mark,
.module:focus-visible .module-mark {
    color: var(--text);
    border-color: var(--arc);
    transform: translateY(-1px) rotate(-4deg) scale(1.06);
}

.module:hover .module-go,
.module:focus-visible .module-go {
    opacity: 1;
    transform: translateX(0);
    color: var(--text-dim);
}

.module:hover .role,
.module:focus-visible .role { color: var(--text); }

/* A tile that is not built yet does not pretend otherwise. */
.module[aria-disabled='true'] .module-mark {
    background: var(--surface);
    color: var(--text-faint);
}

/* Anybody who has asked the system for less movement gets less movement.
 * The colour and the border still answer; only the travel stops. */
@media (prefers-reduced-motion: reduce) {
    .module,
    .module-mark,
    .module-go,
    .module::after { transition-duration: 1ms; }

    .module:hover,
    .module:focus-visible { transform: none; }

    .module:hover .module-mark,
    .module:focus-visible .module-mark { transform: none; }

    .module::after { display: none; }
}

/* ------------------------------------------------------- a row that goes
 *
 * Ninety rows, each with its own button saying the same word. The button was
 * the only thing on the row you could click, which made the other six cells
 * look like they were meant to do something and had stopped working.
 *
 * The row is the link now, and answers a hover the way a tile does. The
 * anchor is real - stretched over the row from the first cell - so middle
 * click opens a tab and the keyboard can reach it, which a click handler on
 * the row would have taken away. */
.rates tbody tr.go {
    position: relative;
    transition: background 140ms ease, box-shadow 140ms ease;
}

.rowgo {
    color: inherit;
    text-decoration: none;
}

/* The part that covers the row. Zero-sized until it is drawn over the
 * whole thing, so it adds nothing to the cell's own layout. */
.rowgo::after {
    content: '';
    position: absolute;
    inset: 0;
    /* Above the cells, below anything that has to stay clickable. */
    z-index: 1;
}

.rates tbody tr.go:hover,
.rates tbody tr.go:focus-within {
    background: var(--surface-hover);
    /* An accent down the leading edge. A row cannot lift the way a tile
     * does without breaking the grid it sits in, so the edge does the work. */
    box-shadow: inset 3px 0 0 var(--violet);
}

.rates tbody tr.go:focus-within { outline: 1px solid var(--arc); outline-offset: -1px; }
.rowgo:focus-visible { outline: none; }

/* The chevron says the row goes somewhere. It is not the control - clicking
 * it works only because the row underneath is the link. */
.gomark {
    color: var(--text-faint);
    opacity: 0;
    transition: opacity 140ms ease, transform 140ms ease;
    transform: translateX(-4px);
}

.rates tbody tr.go:hover .gomark,
.rates tbody tr.go:focus-within .gomark {
    opacity: 1;
    transform: translateX(0);
    color: var(--text-dim);
}

/* Touch has no hover, so the chevron would never appear and the row would
 * look like plain text. Shown always, quietly. */
@media (hover: none) {
    .gomark { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .rates tbody tr.go,
    .gomark { transition-duration: 1ms; }

    .gomark { transform: none; }
}

/* The change form sits above the row's stretched link. Without this the
 * overlay swallows the inputs and the row cannot be edited at all - the
 * failure mode of every clickable-row table that has a control in it. */
.rates tbody tr.go .dfix {
    position: relative;
    z-index: 2;
}

/* ---------------------------------------------------------- mileage chart
 *
 * The office has read distances off a chart for fifty years: airfields down
 * the side, the same across the top, the figure where they meet. A list of
 * "distances from Arusha" answers one question at a time; the chart answers
 * whichever one you happen to have.
 *
 * Ninety-four columns cannot be read at all unless the headings stay put,
 * so both edges are pinned and the cell under the pointer lights its own
 * row and column. Without that, the eye loses the line within three columns
 * and reads off the wrong airfield - which on this screen is a wrong price.
 */
.chart-scroll {
    /* The main column is a flex column, and a flex item is allowed to grow
     * past its container to fit its content unless told otherwise. Without
     * this the chart pushed the whole page sideways instead of scrolling
     * inside its own box. */
    align-self: stretch;
    width: 100%;
    min-width: 0;
    max-width: 100%;
    max-height: 78vh;
    overflow: auto;
    border: var(--edge);
    border-radius: var(--radius);
    background: var(--bg-deep);
    /* The pinned headings are opaque and would otherwise sit on a blurred
     * surface that shows the rows sliding under them. */
    overscroll-behavior: contain;
}

.chart {
    border-collapse: separate;
    border-spacing: 0;
    font-family: var(--font-data);
    font-size: 0.6875rem;
}

.chart th,
.chart td {
    border-right: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
}

/* Down the side. Pinned, so the airfield you are leaving stays in view
 * however far across you scroll. */
.chart tbody th {
    position: sticky;
    left: 0;
    z-index: 2;
    min-width: 8.5rem;
    padding: 0 calc(var(--step) * 1.5);
    background: var(--bg-deep);
    text-align: left;
    font-weight: 600;
    letter-spacing: 0.04em;
}

.chart tbody th a { color: var(--text); text-decoration: none; }
.chart tbody th a:hover { color: var(--violet); }

/* Across the top. Turned on their side, because a code like WEST
 * KILIMANJARO set horizontally would make one column wider than twenty. */
.chart thead th {
    position: sticky;
    top: 0;
    z-index: 3;
    height: 9rem;
    width: 2rem;
    min-width: 2rem;
    padding: calc(var(--step)) 0;
    background: var(--bg-deep);
    vertical-align: bottom;
    font-weight: 600;
}

.chart thead th .turn {
    display: block;
    writing-mode: vertical-rl;
    transform: rotate(180deg);
    letter-spacing: 0.06em;
    color: var(--text-dim);
}

/* The corner belongs to both, so it is pinned to both and sits above both. */
.chart thead th.corner {
    left: 0;
    z-index: 4;
    width: 8.5rem;
    min-width: 8.5rem;
    padding: calc(var(--step) * 1.5);
    vertical-align: bottom;
    text-align: left;
}

.chart thead th.corner span {
    display: block;
    font-family: var(--font-body);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-faint);
    writing-mode: horizontal-tb;
}

.chart td {
    width: 2rem;
    min-width: 2rem;
    padding: calc(var(--step) * 0.75) calc(var(--step) * 0.5);
    text-align: center;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}

/* An airfield against itself. Marked rather than left blank, so it does not
 * read as a figure the chart is missing. */
.chart td.self { color: var(--text-faint); background: var(--chart-self); }

/* A pair the chart never carried. Empty, not zero - and quiet, so the eye
 * finds the figures that do exist rather than the gaps that do not. */
.chart td.none { background: var(--chart-void); }

/* The crosshair. This is the whole reason a chart this wide is readable:
 * the row and the column of whatever is under the pointer both light, so
 * the two headings you are reading between are never in doubt. */
.chart tr.lit > td,
.chart tr.lit > th { background: rgb(108 92 231 / 0.13); }
.chart td.lit,
.chart thead th.lit { background: rgb(108 92 231 / 0.13); }
.chart tr.lit > td.cross { background: rgb(108 92 231 / 0.3); color: var(--text); font-weight: 700; }

.chart tr.lit > th,
.chart thead th.lit .turn { color: var(--text); }

.chart-filter {
    min-height: 34px;
    width: 12rem;
    padding: 0 calc(var(--step) * 1.5);
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg-deep);
    color: var(--text);
    font-family: var(--font-body);
    font-size: 0.8125rem;
}

.chart-filter:focus-visible { border-color: var(--violet); }

/* Rows and columns the filter has put aside. Removed from the flow rather
 * than dimmed: a chart with half its columns greyed is harder to read than
 * the full one. */
.chart .off { display: none; }

/* ---- on paper ----
 *
 * A chart is a thing people pin to a wall. The portal's chrome, the tabs
 * and the ambient arc are all screen furniture; none of it should cost
 * a sheet of A4. */
@media print {
    body { background: #fff; color: #000; }
    .field, header, .tabs, .bar, .note, footer { display: none !important; }

    .chart-scroll {
        max-height: none;
        overflow: visible;
        border: none;
        background: #fff;
    }

    .chart th,
    .chart td {
        border-color: #999;
        background: #fff !important;
        color: #000;
    }

    .chart thead th { position: static; }
    .chart tbody th { position: static; }
    .chart td.none { background: #f2f2f2 !important; }
}

/* Named only on paper. On screen the bar above the chart already says what
 * it is; on paper the bar is gone and a sheet of bare numbers is not a
 * document anybody can file. */
.chart-title { display: none; }

@media print {
    .chart-title {
        display: block;
        margin-bottom: 4pt;
        color: #000;
    }

    .chart-title strong {
        display: block;
        font-size: 9pt;
        line-height: 1.2;
    }

    .chart-title span {
        display: block;
        font-size: 6pt;
        line-height: 1.3;
        color: #444;
    }

    /* Landscape, because ninety-four columns on portrait A4 is nine sheets
     * and a jigsaw. Narrowing the chart with the filter first is the way to
     * get one sheet. */
    @page { size: A4 landscape; margin: 10mm; }

    .chart { font-size: 6pt; }
    .chart thead th { height: 5rem; }
    .chart tbody th { min-width: 0; font-size: 6pt; }
    .chart td { padding: 1pt 2pt; }
}

/* ------------------------------------------------- the chart, full screen
 *
 * The controls live inside the pane rather than in the bar above it, so
 * they travel into full screen with the chart. A filter you cannot reach
 * once the chart fills the screen is a filter that has gone missing at the
 * moment it is most wanted. */
.chart-pane {
    align-self: stretch;
    display: flex;
    flex-direction: column;
    gap: calc(var(--step) * 1.5);
    width: 100%;
    min-width: 0;
}

.chart-tools {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--step);
}

.chart-tools-right {
    display: flex;
    align-items: center;
    gap: var(--step);
}

/* Full screen, whichever way it was reached.
 *
 * :fullscreen is the real API. .expanded is the fallback for browsers that
 * refuse it - iOS Safari offers it for video and nothing else. Both are
 * given a background: the fullscreen backdrop is black by default, which
 * would put a dark sheet behind a light theme. */
.chart-pane:fullscreen,
.chart-pane.expanded {
    padding: calc(var(--step) * 2);
    background: var(--bg);
}

.chart-pane.expanded {
    position: fixed;
    inset: 0;
    z-index: 60;
    margin: 0;
    overflow: auto;
}

/* The box grows to the screen it is now on. 78vh was sized for a chart
 * sitting under a page of other things; here there is nothing else. */
.chart-pane:fullscreen .chart-scroll,
.chart-pane.expanded .chart-scroll {
    max-height: calc(100vh - 6rem);
}

/* ---- the watermark ----
 *
 * Off the screen entirely. It is a thing for paper: a chart that leaves the
 * building should say whose it is. */
.chart-watermark { display: none; }

@media print {
    .chart-watermark {
        display: block;
        position: fixed;
        top: 50%;
        left: 50%;
        width: 170mm;
        max-width: 70%;
        transform: translate(-50%, -50%);
        /* Light, but actually there. The first attempt at 0.06 came out at
         * roughly RGB 241 on white - a fourteen-level difference that is
         * invisible on paper and in a print preview both. A watermark has to
         * be seen to be doing its job; this is still far below the weight of
         * a six-point figure sitting on top of it. */
        opacity: 0.13;
        /* Behind everything, and never in the way of a click that is not
         * happening on paper anyway. */
        z-index: 0;
        pointer-events: none;
    }

    /* The figures sit above it. Without this the watermark is drawn after
     * the table in source order and lands on top of the numbers. */
    .chart-title,
    .chart-pane { position: relative; z-index: 1; }

    /* The cells are given white in the print block above, which would hide
     * the watermark behind them. Transparent instead, so it shows through,
     * and the page itself supplies the white. */
    .chart td,
    .chart th { background: transparent !important; }
    .chart td.none { background: rgb(0 0 0 / 0.05) !important; }
    .chart td.self { background: rgb(0 0 0 / 0.09) !important; }

    /* Nothing in here is a control on paper. */
    .chart-tools { display: none !important; }
}

@media print {
    /* The headings carry their own colours - the row labels are links, the
     * column labels are dimmed - and both are set for a dark screen. Left
     * alone, the entire left-hand column and the whole top row of the
     * printed chart come out near-white on white: a sheet of numbers with
     * nothing saying which airfields they are between. */
    .chart tbody th a,
    .chart thead th .turn,
    .chart td.self {
        color: #000 !important;
    }

    .chart td.self { color: #666 !important; }

    /* The crosshair is a pointer, and there is no pointer on paper. */
    .chart tr.lit > td,
    .chart tr.lit > th,
    .chart td.lit,
    .chart thead th.lit,
    .chart tr.lit > td.cross {
        background: transparent !important;
        font-weight: inherit;
    }
}

/* ------------------------------------------------- the chart, on one sheet
 *
 * The first print of this was unreadable, and the reason was not the font
 * size. The print rules shrank the type to 6pt and left the column widths
 * alone at 2rem, so the table went to press 3,144px wide on a 1,032px page.
 * Chrome does not clip that - it scales the whole sheet to about a third,
 * which turns 6pt into 2pt and leaves a wide gutter down the left where the
 * shrunken page no longer reaches the margin.
 *
 * So the widths are set in millimetres against the paper rather than in
 * rem against a screen. A4 landscape at 8mm margins gives 281mm across:
 * 20mm for the airfield names and 2.75mm for each of the 94 columns comes
 * to 278.5mm, which fits without the browser scaling anything.
 *
 * Two sheets deep rather than one. Ninety-four rows on 194mm would need
 * 4pt, and a chart nobody can read is not a chart. The heading row repeats
 * itself on the second sheet, which is what makes two sheets usable.
 */
@media print {
    /* The page chrome is sized for a screen and eats the margin. */
    .shell { padding: 0; gap: 0; min-height: 0; }
    .middle { max-width: none; gap: 6pt; display: block; }

    .chart-pane { display: block; gap: 0; }

    .chart-scroll {
        max-width: none;
        max-height: none;
        overflow: visible;
        border: none;
        border-radius: 0;
    }

    /* Fixed layout, so the widths below are obeyed rather than treated as
     * suggestions the browser may widen to fit content. */
    .chart {
        table-layout: fixed;
        border-collapse: collapse;
        /* An explicit width, not auto.
         *
         * With a fixed layout and width:auto the browser is free to widen
         * the columns past what they were given - it did, to 3.4mm each,
         * which put the table 46mm over the paper and back into being
         * scaled. Told the total, it divides it exactly.
         *
         * A4 landscape less 8mm margins is 281mm; 278 leaves a hair so a
         * rounding difference cannot tip it onto a second sheet sideways. */
        width: 100%;
        font-size: 4.6pt;
        /* No leading. A mileage chart is a lookup table, not prose - the
         * rows are read across with a finger, and 94 of them with any air
         * between them runs 20mm onto a second sheet. A wall chart in two
         * halves is a wall chart nobody pins up. */
        line-height: 1;
    }

    .chart thead th.corner,
    .chart tbody th {
        width: 20mm;
        min-width: 0;
        max-width: 20mm;
        padding: 0 1mm 0 0;
        font-size: 4.6pt;
        overflow: hidden;
    }

    .chart thead th {
        width: auto;
        min-width: 0;
        height: 14mm;
        padding: 0;
        vertical-align: bottom;
    }

    .chart thead th .turn { font-size: 4.2pt; letter-spacing: -0.01em; }

    /* Ninety-four columns across 281mm of paper leaves 2.8mm each, and
     * three digits have to fit inside that. The data font is monospaced,
     * which is right on screen and costs about half a point here - the
     * body face fits at 4.6pt where the monospaced one needs 4.2pt, and
     * at this size half a point is the difference between reading it and
     * squinting. Tabular figures keep the columns aligned regardless. */
    .chart td {
        width: auto;
        min-width: 0;
        padding: 0 0.1mm;
        font-family: var(--font-body);
        font-size: 4.6pt;
        font-variant-numeric: tabular-nums;
        letter-spacing: -0.01em;
    }

    .chart th,
    .chart td { border: 0.1mm solid #b0b0b0; }

    /* The corner labels are set for a roomy screen box and will not fit
     * 20mm; they are also saying something obvious once it is on paper. */
    .chart thead th.corner span { font-size: 4.6pt; letter-spacing: 0.02em; }

    @page { size: A4 landscape; margin: 8mm; }
}

/* ------------------------------------------------------------ share menu
 *
 * The format is asked before anything is drawn, because the two go to
 * different places: an image is read in a chat thread without opening
 * anything, a PDF goes in an email and prints at the size it was drawn.
 * A single Share button would have to guess, and would be wrong about half
 * the time. */
.chart-share { position: relative; display: inline-flex; }

.chart-share-menu {
    position: absolute;
    top: calc(100% + var(--step));
    right: 0;
    z-index: 20;
    display: flex;
    flex-direction: column;
    min-width: 16rem;
    padding: calc(var(--step) * 0.75);
    border: 1px solid var(--arc);
    border-radius: 12px;
    background: var(--bg-deep);
    box-shadow: 0 14px 34px rgb(4 6 20 / 0.5);
}

.chart-share-menu[hidden] { display: none; }

.chart-share-menu button {
    display: flex;
    align-items: flex-start;
    gap: calc(var(--step) * 1.25);
    padding: calc(var(--step) * 1.25);
    border: 0;
    border-radius: 8px;
    background: none;
    color: var(--text);
    font-family: var(--font-body);
    text-align: left;
    cursor: pointer;
}

.chart-share-menu button:hover,
.chart-share-menu button:focus-visible { background: var(--surface-hover); }

.chart-share-menu .icon { margin-top: 2px; color: var(--text-dim); flex: none; }

.chart-share-menu strong {
    display: block;
    font-size: 0.875rem;
    font-weight: 600;
}

.chart-share-menu span span,
.chart-share-menu button > span {
    display: block;
    font-size: 0.75rem;
    color: var(--text-dim);
    line-height: 1.4;
}

.chart-share-menu button > span strong { color: var(--text); }

@media print { .chart-share-menu { display: none !important; } }

/* The clickable row, on the customer list as well as the airfield list.
 * Same behaviour, so it only has to be learned once. */
.compare tbody tr.go {
    position: relative;
    transition: background 140ms ease, box-shadow 140ms ease;
}

.compare tbody tr.go:hover,
.compare tbody tr.go:focus-within {
    background: var(--surface-hover);
    box-shadow: inset 3px 0 0 var(--violet);
}

.compare tbody tr.go:hover .gomark,
.compare tbody tr.go:focus-within .gomark {
    opacity: 1;
    transform: translateX(0);
    color: var(--text-dim);
}

/* ------------------------------------------------------ the customers, seen
 *
 * A count of active customers says nothing on its own - eleven is a lot or a
 * little depending on whether the other ninety went quiet. The ring is every
 * customer ever added; the pie inside it is how that splits today. */
.census {
    display: flex;
    align-items: center;
    gap: calc(var(--step) * 4);
    flex-wrap: wrap;
    padding: calc(var(--step) * 3);
    border: var(--edge);
    border-radius: var(--radius);
    background: var(--surface);
    backdrop-filter: blur(8px) saturate(135%);
    -webkit-backdrop-filter: blur(8px) saturate(135%);
}

.census-pie { width: 9rem; height: 9rem; flex: none; }

.census-pie .ring { stroke: var(--arc); }
.census-pie .active { fill: var(--violet); }
.census-pie .retired { fill: var(--text-faint); }

.census-pie .total {
    fill: var(--text);
    font-family: var(--font-data);
    font-size: 13px;
    font-weight: 700;
    text-anchor: middle;
    /* On the ring rather than in the middle: the middle belongs to the
     * split, and a number sitting in it reads as one of the two shares. */
    paint-order: stroke;
    stroke: var(--bg);
    stroke-width: 4px;
}

.census-key { display: flex; flex-direction: column; gap: calc(var(--step) * 0.75); }

.census-key p {
    margin: 0;
    font-size: 0.875rem;
    color: var(--text-dim);
    display: flex;
    align-items: center;
    gap: calc(var(--step) * 1.25);
}

.census-total { font-size: 1rem; color: var(--text); }
.census-total strong { font-family: var(--font-data); font-size: 1.375rem; }

.census-key .dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    flex: none;
}

.census-key .dot.active { background: var(--violet); }
.census-key .dot.retired { background: var(--text-faint); }

/* -------------------------------------------------------------- a dialog
 *
 * Adding a customer is a two-minute job done in the middle of
 * something else. A page of its own loses whatever you were looking at.
 *
 * The native element rather than a hand-rolled overlay: it gives the
 * backdrop, Escape, focus held inside, and inertness of the page behind -
 * four things every hand-rolled modal gets wrong at least one of. */
.modal {
    width: min(56rem, calc(100vw - var(--step) * 4));
    max-height: calc(100dvh - var(--step) * 4);
    padding: 0;
    border: var(--edge);
    border-radius: var(--radius);
    background: var(--bg);
    color: var(--text);
    overflow: auto;
    overscroll-behavior: contain;
}

.modal::backdrop {
    background: rgb(4 6 20 / 0.62);
    backdrop-filter: blur(3px);
}

/* Above the sticky bar, which was painting straight over it.
 *
 * These dialogs are opened by rendering the `open` attribute from PHP rather
 * than by calling showModal(), and `open` makes a NON-modal dialog: no
 * backdrop, no top layer, and z-index auto. The action bar that sits above
 * every list is `position: sticky; z-index: 20`, so it won - and the middle of
 * every add-and-edit form in the portal was covered by it.
 *
 * Invisible on a wide screen, where the form is tall and centred and the bar
 * is a strip near the top. On a phone the two occupy the same few hundred
 * pixels and the form simply is not there: header visible, fields gone.
 */
.modal[open] {
    z-index: 100;
}

/* The sheet inside loses its own frame - the dialog is the frame now. */
.modal .sheet {
    margin: 0;
    border: 0;
    border-radius: 0;
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

/* Save and Cancel stay reachable however long the form is. */
.modal .sheet-head {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--bg);
}

.modal .retire {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--step);
    padding: calc(var(--step) * 3);
    border-top: var(--edge);
}

/* ---- two blocks, side by side ----
 *
 * Where a customer is and how they are billed are different questions, and
 * neither is long enough to need the whole width. Below the breakpoint they
 * stack, because two columns on a phone is two half-columns. */
.pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: calc(var(--step) * 2);
    align-items: start;
}

@media (max-width: 52rem) {
    .pair { grid-template-columns: 1fr; }
}

/* ---- on a phone ----
 *
 * The portal is used on a handset as much as at a desk. A dialog that keeps
 * desk proportions on a 375px screen is a postage stamp with a scrollbar,
 * so it takes the whole screen instead - which is what a phone dialog is
 * everywhere else on the device. */
@media (max-width: 40rem) {
    .modal {
        width: 100vw;
        max-width: 100vw;
        height: 100dvh;
        max-height: 100dvh;
        margin: 0;
        border: 0;
        border-radius: 0;
    }

    .modal .sheet-head {
        flex-wrap: wrap;
        gap: var(--step);
    }

    /* Save is the one that has to be easy to hit with a thumb. */
    .modal .sheet-head .head-right { width: 100%; }
    .modal .sheet-head .action.primary { flex: 1; justify-content: center; }

    .census { gap: calc(var(--step) * 2); justify-content: center; }
    .census-pie { width: 7rem; height: 7rem; }
}

/* ------------------------------------------------------------------ payroll
 *
 * The month's totals above the table rather than at the foot of it. The
 * figure everybody opens this screen for is the net payable, and putting it
 * below fifty rows means scrolling past fifty rows to find it. */
.totals-strip {
    align-self: stretch;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
    gap: 1px;
    background: var(--border);
    border: var(--edge);
    border-radius: var(--radius);
    overflow: hidden;
}

.totals-strip > div {
    padding: calc(var(--step) * 2) calc(var(--step) * 2.5);
    background: var(--surface);
}

.totals-strip span {
    display: block;
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--text-faint);
}

.totals-strip strong {
    display: block;
    margin-top: 3px;
    font-family: var(--font-data);
    font-size: 1.0625rem;
    color: var(--text);
}

/* The one figure the month is about. */
.totals-strip .lead { background: rgb(108 92 231 / 0.14); }
.totals-strip .lead strong { color: var(--text); font-size: 1.1875rem; }

/* The grouped header row, as the payroll sheet groups it: what somebody earns,
 * what comes off, what is left. */
.payroll thead tr.group th {
    padding-bottom: calc(var(--step) * 0.5);
    border-bottom: 1px solid var(--border);
    font-size: 0.625rem;
    letter-spacing: 0.12em;
    color: var(--text-faint);
}

.payroll th.num, .payroll td.num { text-align: right; }
.payroll td.strong { color: var(--text); font-weight: 600; }
.payroll .unit { font-size: 0.6875rem; color: var(--text-faint); }

/* A figure typed straight into the table. Fifty people is one sitting, and
 * a dialog per row would make it fifty sittings. */
.cell {
    width: 6rem;
    min-height: 30px;
    padding: 0 calc(var(--step));
    border: 1px solid transparent;
    border-radius: 6px;
    background: rgb(255 255 255 / 0.04);
    color: var(--text);
    font-family: var(--font-data);
    font-size: 0.8125rem;
    text-align: right;
}

.cell.narrow { width: 3.25rem; }
.cell:hover { border-color: var(--border); }
.cell:focus-visible { border-color: var(--violet); background: var(--bg-deep); outline: none; }

:root[data-theme='light'] .cell { background: rgb(25 34 98 / 0.04); }

/* Month pickers. */
.pick { display: flex; flex-direction: column; gap: 3px; }

.pick > span {
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-faint);
}

.pick select,
.pick input {
    min-height: 34px;
    padding: 0 calc(var(--step) * 1.25);
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg-deep);
    color: var(--text);
    font-family: var(--font-body);
    font-size: 0.8125rem;
}

@media (max-width: 40rem) {
    .totals-strip { grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr)); }
    .cell { width: 4.5rem; }
}

/* A wide table scrolls inside itself at every width, not only on a phone.
 *
 * The rule above only turned this on below 720px, which was right when the
 * widest table was the airfield list. A payroll is fourteen columns and
 * overflows a laptop too, and a table that pushes the whole page sideways
 * takes the heading and the buttons with it. */
.table-scroll {
    align-self: stretch;
    width: 100%;
    min-width: 0;
    max-width: 100%;
    overflow-x: auto;
    overscroll-behavior-x: contain;
}

/* ------------------------------------------------------- the salary slip
 *
 * Laid out as the payroll sheet's Salary Voucher, because that is the document
 * everybody already knows how to read. Two to a sheet of A4, which is how
 * they are handed out at the end of the month. */
.slips {
    align-self: stretch;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
    gap: calc(var(--step) * 3);
    width: 100%;
}

.voucher {
    display: flex;
    flex-direction: column;
    border: var(--edge);
    border-radius: var(--radius);
    background: var(--surface);
    overflow: hidden;
}

.voucher-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--step);
    padding: calc(var(--step) * 2.5);
    background:
        linear-gradient(135deg, rgb(108 92 231 / 0.2), transparent 70%),
        var(--surface);
    border-bottom: 1px solid var(--border);
}

.voucher-head strong { display: block; font-family: var(--font-display); font-size: 0.9375rem; }
.voucher-head span { display: block; font-size: 0.6875rem; color: var(--text-dim); line-height: 1.5; }

.voucher-period { text-align: right; flex: none; }
.voucher-period span { font-size: 0.5625rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-faint); }
.voucher-period strong { font-size: 0.875rem; white-space: nowrap; }

.voucher-who {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: calc(var(--step) * 1.5);
    margin: 0;
    padding: calc(var(--step) * 2.5);
    border-bottom: 1px solid var(--border);
}

.voucher-who dt {
    font-size: 0.5625rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-faint);
}

.voucher-who dd { margin: 2px 0 0; font-size: 0.8125rem; color: var(--text); }

.voucher-lines { width: 100%; border-collapse: collapse; }
.voucher-lines td, .voucher-lines th { padding: calc(var(--step) * 0.85) calc(var(--step) * 2.5); }
.voucher-lines td { font-size: 0.8125rem; color: var(--text-dim); }
.voucher-lines .num { text-align: right; color: var(--text); white-space: nowrap; }

.voucher-lines tr.section th {
    padding-top: calc(var(--step) * 2);
    font-size: 0.5625rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    text-align: left;
    color: var(--text-faint);
}

.voucher-lines tr.total td {
    border-top: 1px solid var(--border);
    font-weight: 600;
    color: var(--text);
}

/* The one figure anybody actually looks for. */
.voucher-lines tr.net td {
    padding-top: calc(var(--step) * 1.5);
    padding-bottom: calc(var(--step) * 1.5);
    border-top: 2px solid var(--arc);
    background: rgb(108 92 231 / 0.12);
    font-size: 0.9375rem;
    font-weight: 700;
    color: var(--text);
}

.voucher-lines .none { color: var(--text-faint); font-style: italic; }

.voucher-foot {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--step);
    margin-top: auto;
    padding: calc(var(--step) * 2.5);
    border-top: 1px solid var(--border);
    font-size: 0.6875rem;
    color: var(--text-faint);
}

/* Room to sign, because these are signed for. */
.voucher-foot .sign {
    min-width: 9rem;
    padding-top: calc(var(--step) * 2);
    border-bottom: 1px dotted var(--text-faint);
    text-align: center;
}

@media print {
    .slips { grid-template-columns: 1fr 1fr; gap: 8mm; }

    .voucher {
        border: 0.2mm solid #999;
        border-radius: 0;
        background: #fff;
        color: #000;
        break-inside: avoid;
    }

    .voucher-head { background: #fff; border-bottom: 0.2mm solid #999; }
    .voucher-head strong, .voucher-who dd, .voucher-lines .num { color: #000; }
    .voucher-head span, .voucher-who dt, .voucher-lines tr.section th { color: #444; }
    .voucher-lines td { color: #222; }
    .voucher-lines tr.net td { background: #f0f0f0; border-top: 0.4mm solid #000; color: #000; }

    @page { size: A4 portrait; margin: 10mm; }
}

/* A note that is prose rather than a row of parts.
 *
 * `.note` is a flex row, which is right when it holds an icon beside a line
 * of text. Put a paragraph with emphasis inside it and every <strong>
 * becomes a flex item on the same non-wrapping line - so a note carrying
 * half a dozen figures ran off the side of a phone while reporting that it
 * fitted. Prose wants a block.
 */
.note.prose {
    display: block;
}

.note.prose strong {
    color: var(--text);
    font-family: var(--font-data);
    font-weight: 600;
    white-space: nowrap;
}

/* Kept whole so a figure is never broken across lines - but on a phone a
 * strong that is a whole sentence, "A cancelled serial is still a filed
 * serial.", is wider than the screen held on one line, and pushed the
 * Cancelled page 13 pixels sideways (found 22 October 2026). There it
 * wraps like the words around it. */
@media (max-width: 40rem) {
    .note.prose strong { white-space: normal; }
}

/* Somewhere to sign for cash. The list is printed and signed as the
 * envelopes are handed out. */
.sign-line {
    min-width: 11rem;
    border-bottom: 1px dotted var(--text-faint);
}

@media print {
    .sign-line { border-bottom: 0.2mm dotted #666; }
}

/* A prose note that is a warning. `.note.warn` inherits the flex row from
 * `.note`, which turns a paragraph with emphasis in it into a single
 * non-wrapping line - so it needs the same block treatment as `.note.prose`
 * when it carries prose rather than a single line beside an icon. */
.note.prose.warn strong { color: var(--caution); }

/* ------------------------------------------------------- summary panels
 *
 * Two boxes side by side, stacking on a phone. The same shape as the pair
 * of blocks on the customer form, because they do the same job: two things
 * that are read together and neither of which needs the whole width.
 *
 * clip, not hidden, as table.rates has it: both trim what is inside to the
 * rounded corners, but hidden made every panel a scroll box of its own, and
 * a held heading holds to the nearest one - a panel that never scrolls - so
 * no list in a panel kept its headings under the pinned row: a consignment's
 * Parts, quote approvals, Costs and Landed cost, the payroll summary's cost
 * list (review, 28 October 2026). A list wider than its panel still scrolls
 * sideways in its own .table-scroll. hidden first, for a browser that does
 * not know clip. */
.panel {
    display: flex;
    flex-direction: column;
    border: var(--edge);
    border-radius: var(--radius);
    background: var(--surface);
    overflow: hidden;
    overflow: clip;
}

.panel-head {
    padding: calc(var(--step) * 2) calc(var(--step) * 2.5);
    border-bottom: 1px solid var(--border);
    background:
        linear-gradient(135deg, rgb(108 92 231 / 0.16), transparent 70%),
        var(--surface);
}

.panel-head strong {
    display: block;
    font-family: var(--font-display);
    font-size: 0.9375rem;
}

.panel-head span {
    display: block;
    margin-top: 2px;
    font-size: 0.6875rem;
    color: var(--text-dim);
    line-height: 1.45;
}

.rates.tight td { padding: calc(var(--step) * 1.1) calc(var(--step) * 2); }
.rates.tight .rank { width: 2rem; color: var(--text-faint); font-family: var(--font-data); font-size: 0.75rem; }

/* A bar for a count. Cheaper to read than the number alone, and it needs no
 * chart library, no script and no colour beyond the one already in use. */
.bands { padding: calc(var(--step) * 2) calc(var(--step) * 2.5); }

.band {
    display: grid;
    grid-template-columns: 7.5rem 1fr auto 2rem;
    align-items: center;
    gap: calc(var(--step) * 1.5);
    padding: calc(var(--step) * 0.6) 0;
}

.band-label { font-size: 0.75rem; color: var(--text-dim); }

.band-bar {
    display: block;
    height: 9px;
    border-radius: 999px;
    background: rgb(255 255 255 / 0.06);
    overflow: hidden;
}

.band-bar > span {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: var(--violet);
    /* A band with nobody in it still shows the track, not a stray dot. */
    min-width: 0;
}

.band-bar.wide { min-width: 6rem; }

:root[data-theme='light'] .band-bar { background: rgb(25 34 98 / 0.08); }

.band-count {
    font-family: var(--font-data);
    font-size: 0.8125rem;
    text-align: right;
    color: var(--text);
}

/* The flight row opens the record, but three Attach controls sit on it.
 *
 * The row's link is a stretched overlay, and an overlay is exactly what
 * swallows a control underneath it - the failure mode of every clickable-row
 * table that has a button in it. Lifting the slots above the overlay is what
 * keeps Attach, View and remove working while the rest of the row navigates.
 */
.flights tbody tr.go .slot,
.flights tbody tr.go form {
    position: relative;
    z-index: 2;
}

/* The fuel note number, typed on the row.
 *
 * Under the attach control, because the number and the docket are read off
 * the same piece of paper at the same moment. Above the row's stretched
 * link, or the box could not be typed into at all. */
.note-no {
    /* Beside the attach control, not under it. Stacking them made the
     * fuel note cell twice the height of every other cell, which set the
     * height of every row in the table. */
    display: inline-flex;
    align-items: center;
    gap: calc(var(--step) * 0.5);
    margin-left: calc(var(--step) * 0.75);
    vertical-align: middle;
    position: relative;
    z-index: 2;
}

/* Short, because a docket number is. Matching the attach control's
 * height keeps the pair on one line of the row. */
.cell.wide {
    width: 5.5rem;
    min-height: 30px;
    text-align: left;
}

/* Only worth reaching for once something has been typed. */
.note-no .linkish {
    opacity: 0;
    transition: opacity 140ms ease;
    font-size: 0.6875rem;
}

.note-no:hover .linkish,
.note-no:focus-within .linkish { opacity: 1; }

/* The attach control's form is a block, so it claimed the whole line and
 * pushed the fuel note number underneath however the number itself was
 * styled. Inline, the pair sit side by side and the row keeps one line's
 * height - which sets the height of every row in the table. */
.flights .attach {
    display: inline-flex;
    vertical-align: middle;
}

/* A failure of the dialog's own form, said inside the dialog. */
.sheet-flash {
    margin: 0 calc(var(--step) * 3) calc(var(--step) * 2);
}

/* ------------------------------------------------- tables, everywhere
 *
 * Headings and their columns sit on one axis, and which axis is decided by
 * what the column holds - not by where it sits.
 *
 * That distinction is the whole point. This started as
 * `.flights thead th:nth-child(n + 3) { text-align: center }`, written for
 * the flight list, and then every table that borrowed the class inherited an
 * alignment meant for somebody else's columns: the passenger list ended up
 * with SEAT centred over a flight count.
 *
 * The convention, and it holds for any table added later:
 *
 *   nothing      text. Read down the column, so read from its first letter.
 *   .num         a figure. Centred, heading centred over it.
 *   .mid .doc    a control, or a column that should centre anyway.
 *
 * .data is deliberately not in that list. It means tabular monospace -
 * a font, not a position - and the serial on the flight list carries it
 * while being read down the column like a name. Treating it as "centre
 * this" is what put SERIAL left and its own figures in the middle.
 *
 * A heading takes .mid when its column carries a figure or a control. The
 * body cells already carry these classes; the head now says the same thing,
 * so nothing has to know its own position.
 */
.rates th,
.rates td {
    text-align: left;
}

.rates th.mid,
.rates th.num,
.rates td.num,
.rates td.mid,
.rates td.doc {
    text-align: center;
}

/* Except where a table has said otherwise. The payroll and the quotation
 * comparison right-align their money, which is how a column of figures is
 * read - from the last digit - and their headings now carry the same class
 * so both move together. */
.payroll th.num { text-align: right; }

/* A cell holding controls is a flex row, and a flex row ignores text-align. */
.rates td.doc {
    white-space: nowrap;
}

.rates td.doc > * {
    vertical-align: middle;
}

.rates .attach,
.rates .slot {
    justify-content: center;
}

/* ------------------------------------------------------------ passcode */

/* Asked for at the moment it is needed, on whichever screen somebody is
 * standing on. That is the reason everything here is self-contained: it
 * borrowed the quotation sheet's form styling once, appeared on Settings -
 * which loads only this file - and collapsed into unstyled text over the
 * tiles behind it.
 *
 * A single question with a single answer, so it is sized like one rather
 * than like a form. */
.passcode {
    width: min(24rem, calc(100vw - var(--step) * 4));
    padding: 0;
    border: var(--edge);
    border-radius: var(--radius);
    background: var(--bg);
    color: var(--text);
}

.passcode::backdrop {
    background: rgb(4 6 20 / 0.62);
    backdrop-filter: blur(3px);
}

/* The server writes `open`, which makes a dialog non-modal: no backdrop, and
 * laid out in the flow of the page rather than over it. passcode.js shuts it
 * and reopens it properly. Until that runs - or if it never does - this
 * keeps it over the page and legible rather than sitting among the tiles. */
.passcode:not(:modal) {
    position: fixed;
    inset: 50% auto auto 50%;
    transform: translate(-50%, -50%);
    z-index: 50;
    box-shadow: 0 24px 60px rgb(4 6 20 / 0.55);
}

.passcode form {
    display: flex;
    flex-direction: column;
    gap: calc(var(--step) * 1.5);
    padding: calc(var(--step) * 3);
}

.passcode h2 {
    margin: 0;
    font-family: var(--font-display);
    font-size: 1.0625rem;
    font-weight: 600;
}

/* What is about to happen. The whole reason the box is on screen, so it
 * reads as a sentence rather than as a field label. */
.passcode-for {
    margin: calc(var(--step) * -1) 0 0;
    color: var(--text-dim);
    font-size: 0.875rem;
    line-height: 1.45;
}

.passcode-box {
    width: 100%;
    min-height: 42px;
    padding: 0 calc(var(--step) * 1.5);
    border: 1px solid var(--border);
    border-radius: 9px;
    background: var(--surface);
    color: var(--text);
    font-family: var(--font-data);
    font-size: 1.125rem;
    letter-spacing: 0.3em;
}

.passcode-box:focus-visible {
    border-color: var(--violet);
    outline: 2px solid var(--arc);
    outline-offset: 1px;
}

.passcode-box:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.passcode-note {
    margin: calc(var(--step) * -1) 0 0;
    color: var(--text-faint);
    font-size: 0.75rem;
}

.passcode-problem {
    margin: 0;
    padding: calc(var(--step) * 1.25) calc(var(--step) * 1.5);
    border: 1px solid rgb(255 90 78 / 0.4);
    border-radius: 9px;
    background: rgb(255 90 78 / 0.12);
    color: #ff8578;
    font-size: 0.8125rem;
    line-height: 1.45;
}

.passcode-do {
    display: flex;
    justify-content: flex-end;
    gap: var(--step);
    margin-top: calc(var(--step) * 0.5);
}

@media (max-width: 30rem) {
    /* Thumbs, not a mouse. Full-width targets in the order they are used. */
    .passcode-do {
        flex-direction: column-reverse;
    }

    .passcode-do .action {
        justify-content: center;
    }
}

/* ------------------------------------------------ the payroll summary */

/* A list of ten names beside everything that reads as a chart.
 *
 * The list needs the width of its own three columns and no more - given
 * half the screen it opened a hand's width of nothing between each name and
 * its figure. fit-content lets it take what it needs and stop, with a
 * ceiling so a long job title cannot run away with the row.
 */
.summary-grid {
    display: grid;
    grid-template-columns: fit-content(30rem) minmax(0, 1fr);
    gap: calc(var(--step) * 2);
    align-items: start;
    margin-top: calc(var(--step) * 2);
}

.summary-left,
.summary-right {
    display: flex;
    flex-direction: column;
    gap: calc(var(--step) * 2);
    min-width: 0;
}

/* The bar and the table below it belong to each other, so they lose the
 * gap between them and read as one block. */
.summary-right .bar {
    margin-bottom: 0;
}

/* Names and figures, with only the space between them that is needed to
 * tell them apart. */
.summary-left .rates.tight td:first-child { width: 1%; }
.summary-left .rates.tight td:last-child { width: 1%; white-space: nowrap; }

/* Below a laptop the two columns become one, in reading order: who earns
 * most, how take-home spreads, then what each part of the company costs. */
@media (max-width: 64rem) {
    .summary-grid {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* What each band of take-home actually costs.
 *
 * Scaled by money, in bracket order. That only works because the brackets
 * above two million were split: with a single "2m and above" holding 158m
 * of the 184m, one bar took the full width and the rest were slivers.
 */
.band-money {
    font-family: var(--font-data);
    font-variant-numeric: tabular-nums;
    font-size: 0.8125rem;
    text-align: right;
    white-space: nowrap;
    color: var(--text-dim);
}

.band-count {
    color: var(--text-faint);
}

/* On a phone four columns leave the bar about fifty pixels wide, which is
 * not a bar. The label takes the line above and the bar keeps the width. */
@media (max-width: 40rem) {
    .band {
        grid-template-columns: 1fr auto 2rem;
        row-gap: calc(var(--step) * 0.4);
    }

    .band-label {
        grid-column: 1 / -1;
    }
}

/* The cost heading and the table it heads, as one thing.
 *
 * They were two bordered boxes stacked, which reads as two unrelated panels -
 * and a heading that looks unrelated to the table under it is worse than no
 * heading at all.
 */
.panel.cost > .bar {
    margin: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    border-bottom: 1px solid var(--border);
}

.panel.cost .table-scroll {
    margin: 0;
}


/* A band row is a button now, because pressing it opens who is in it.
 * It must not look like one - it is a chart row - so the button's own
 * furniture is taken off and only the hover is added. */
button.band {
    width: 100%;
    border: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
    border-radius: 8px;
    transition: background 120ms ease;
}

button.band:hover,
button.band:focus-visible {
    background: var(--surface-hover);
}

button.band:focus-visible {
    outline: 2px solid var(--arc);
    outline-offset: -2px;
}

/* Who is in the slice that was pressed. The general table convention above
 * does the aligning; nothing to say here. */

/* A figure nobody recorded, said rather than left blank - an empty cell
 * reads as a person with no nationality. */
.rates.people .unknown {
    color: var(--text-faint);
    font-size: 0.8125rem;
    font-style: italic;
}

/* Add another, and Record them, on one line at the foot of the form. */
.manifest-do {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--step);
    flex-wrap: wrap;
}

@media (max-width: 30rem) {
    .manifest-do { flex-direction: column-reverse; align-items: stretch; }
    .manifest-do .action { justify-content: center; }
}

/* A control on a row, above the row's own stretched link - or pressing it
 * would open the record instead of doing what it says. */
.flights .rowdo {
    position: relative;
    z-index: 2;
    white-space: nowrap;
}

.flights .rowdo .linkish {
    opacity: 0;
    transition: opacity 120ms ease;
}

/* Shown on hover, like the chevron. Cancelling is rare, and a row that
 * offers it permanently reads as a row asking to be cancelled. */
.flights tbody tr.go:hover .rowdo .linkish,
.flights tbody tr.go:focus-within .rowdo .linkish { opacity: 1; }

@media (hover: none) {
    .flights .rowdo .linkish { opacity: 1; }
}

.linkish.danger { color: var(--alarm); }

/* ------------------------------------------------- finding a briefing */

/* Shut by default. Somebody arriving wants the last few flights, not a form -
 * and a filter panel permanently open is a screen that looks like work. */
.filters {
    margin-bottom: calc(var(--step) * 2);
    border: var(--edge);
    border-radius: var(--radius);
    background: var(--surface);
}

.filters > summary {
    display: flex;
    align-items: center;
    gap: calc(var(--step) * 0.75);
    padding: calc(var(--step) * 1.5) calc(var(--step) * 2);
    cursor: pointer;
    font-family: var(--font-data);
    font-size: 0.6875rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--text-faint);
    user-select: none;
}

.filters > summary:hover { color: var(--text-dim); }
.filters[open] > summary { border-bottom: 1px solid var(--border); }

.filter-form {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: calc(var(--step) * 1.5);
    padding: calc(var(--step) * 2);
    align-items: end;
}

.filter-form label {
    display: flex;
    flex-direction: column;
    gap: calc(var(--step) * 0.5);
    min-width: 0;
}

.filter-form label > span {
    font-family: var(--font-data);
    font-size: 0.625rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-faint);
}

.filter-do {
    display: flex;
    gap: var(--step);
    align-items: end;
}

@media (max-width: 30rem) {
    .filter-form { grid-template-columns: minmax(0, 1fr); }
    .filter-do { flex-direction: column-reverse; }
    .filter-do .action { justify-content: center; }
}

/* ---------------------------------------------- the role access grid
 *
 * Sections and screens down the left, five ticks across. Its own rules
 * because a checkbox here is a tick and not a field: the form styling above
 * stretches an input to fill its container, which is right for a name box
 * and turns a tick into a 116-pixel bar, five of them per row.
 */
.rates.access input[type="checkbox"] {
    /* Drawn by the browser, not by us.
     *
     * `.rates input` above gives every input in a table a deep background and
     * a border, which is right for the rate box it was written for and turns a
     * checkbox into a filled dark square - identical whether it is ticked or
     * not, on the light theme especially. Reset here so the tick is a tick. */
    appearance: auto;
    -webkit-appearance: checkbox;
    background: none;
    border: none;
    box-shadow: none;
    padding: 0;

    width: 17px;
    height: 17px;
    min-height: 0;
    min-width: 0;
    flex: 0 0 auto;
    margin: 0;

    /* Without this they are the browser's own, which on the light theme comes
     * out as a row of dark filled squares - unreadable as ticked or not, and
     * the first thing anybody said about this screen. Every other checkbox in
     * the portal already gets the violet; this table was outside the rule that
     * hands it out. */
    accent-color: var(--violet);
    cursor: pointer;
}

/* The grid is taller than a dialog, so it scrolls inside one and the heading
 * with the role's name and the Save button stay put above it.
 *
 * Hussein, 15 October 2026: "the headers hide when scrolled down, and also,
 * the details could be below when scrolled down, not locked to be seen at all
 * times". The scrolling box was a fixed 60% of the screen with the note about
 * View and Cancel pinned under it, so a good part of every visit to this
 * dialog was spent looking at a paragraph that is read once. Now the dialog
 * is a column - the title bar, then one scrolling box that takes all the room
 * left - and the note is the last thing in that box, there when he scrolls to
 * the end of the list and out of the way until then.
 *
 * One box scrolls both ways. On a phone the grid is wider than the screen,
 * and a heading can only stay pinned to the box it scrolls in: a separate
 * sideways box round the table would take the headings with it again.
 *
 * --grid-edge lines the first column up with the title above it, since the
 * grid now runs to the dialog's edges instead of sitting in a frame of its own.
 */
#access-dialog { --grid-edge: calc(var(--step) * 3); }

#access-dialog[open] {
    /* Held to the window, a step below its top edge, rather than left where
     * the page put it. Rendered with `open`, the dialog sits in the flow under
     * the header, and once it grows to the height of the window that leaves
     * the bottom of the list and the note below the fold. Pinned at the top
     * rather than centred like the costing sheet: a section switch grows and
     * shrinks the list, and a centred dialog would move the title bar - and
     * the switch under his finger - every time one was ticked. */
    position: fixed;
    inset: calc(var(--step) * 2) 0 auto;
    margin: 0 auto;

    display: flex;
    flex-direction: column;

    /* Nothing in it is taller than it any more: the box below does the
     * scrolling, and a scrollbar round a scrollbar is one too many. */
    overflow: hidden;

    /* Below the passcode box (50), as the person dialog is (#user-dialog,
     * review 15 October 2026): pinned and as tall as the window, at the 100
     * every dialog is given it covered that box, and Save access asked for a
     * passcode nobody could see where to type. Still above the header (30),
     * the sticky bar (20) and the tabs (19). */
    z-index: 40;
}

#access-dialog .sheet {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

#access-dialog .sheet-head { flex: none; }

.access-wrap {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    overscroll-behavior: contain;

    /* Tabbing back up the grid brings the tick to rest below the pinned
     * headings, not behind them, so the focus ring is never hidden. */
    scroll-padding-top: 3rem;
}

/* visible, not the hidden every other table has. "hidden" made the table a
 * box of its own for anything pinned inside it, so the headings were pinned
 * to a table that never scrolls and went off the top with the rows - which is
 * the "headers hide" Hussein saw. */
.access-wrap table.rates {
    border: none;
    border-radius: 0;
    overflow: visible;
}

/* The header row has to stay visible: fifteen rows down, nothing else says
 * which column is Approve and which is Cancel.
 *
 * Solid, because the rows pass underneath it: the table's own tint laid over
 * the dialog's colour, so it looks exactly as it does standing still, in
 * either theme. The line under it is a shadow because a table's shared
 * borders stay where they were drawn when the cell above them moves. */
.rates.access thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: linear-gradient(var(--surface), var(--surface)), var(--dialog-bg);
    box-shadow: inset 0 -1px 0 var(--border);
}

.rates.access thead th:first-child,
.rates.access tr.section td { padding-left: var(--grid-edge); }

/* At the end of the list, set in from the edges like the title.
 *
 * Held at the left edge when the grid has been scrolled sideways on a phone,
 * so reaching the bottom with the Cancel column in view still shows the
 * whole note rather than its last few words. Nothing moves on a wider
 * screen, where there is no sideways scroll. */
.access-wrap > .note {
    position: sticky;
    left: var(--grid-edge);
    margin: calc(var(--step) * 2.5) var(--grid-edge) var(--grid-edge);
}

/* On a phone the dialog is the whole screen, so it starts at the very top;
 * and its title bar comes in to 16 pixels from the edge, so the grid and the
 * note come in with it. */
@media (max-width: 40rem) {
    #access-dialog { --grid-edge: 16px; }
    #access-dialog[open] { top: 0; }
}

.rates.access tr.section label {
    display: inline-flex;
    align-items: center;
    gap: 0.6em;
}

.rates.access tr.section label span {
    font-weight: 400;
    color: var(--text-faint);
    font-size: 0.8rem;
}

.rates.access .na { color: var(--text-faint); }

/* What a tick means on a row whose column name alone would mislead
 * (config/areas.php 'tick_note'). Read before ticking, so the dim colour
 * rather than the faint one. */
.rates.access .tick-note {
    display: block;
    margin-top: 2px;
    color: var(--text-dim);
    font-size: 0.8rem;
}

/* Tight rows.
 *
 * The table padding is set for lists of a dozen flights, where the row is a
 * line of prose and wants air. This is seventeen rows of a word and five
 * ticks; at the same spacing four screens filled the dialog and the rest was
 * scrolling. A grid is read down a column, and the columns want to be close
 * enough to read across.
 */
.rates.access td,
.rates.access th {
    padding-top: calc(var(--step) * 0.6);
    padding-bottom: calc(var(--step) * 0.6);
}

/* The section switch reads as a heading, so it is given the weight of one
 * and the rows beneath are indented under it. */
.rates.access tr.section td {
    background: var(--surface-2, rgba(255, 255, 255, 0.03));
    padding-top: calc(var(--step) * 0.9);
    padding-bottom: calc(var(--step) * 0.9);
}

.rates.access tr:not(.section) td:first-child {
    padding-left: calc(var(--grid-edge, 0.8rem) + 1.25rem);
}

/* A tick column is only as wide as a tick. Without this the five columns
 * take the width the header words want and the screen names get squeezed. */
.rates.access td.mid,
.rates.access th.mid {
    width: 4.5rem;
    text-align: center;
}

/* --------------------------------------- a person: Add someone, Amend
 *
 * Hussein, 15 October 2026: "make this screen dynamic as well". The fields
 * were laid out by .inputs, which fits in as many 7.5rem columns as the width
 * allows - and a 56rem dialog allows six, so all six fields went in one row a
 * hand-span each: the dropdowns read "No role ye..." and "All - ever...", and
 * a long note was held under them.
 *
 * The same shape as the roles dialog above. The dialog is a column - the
 * title bar with Cancel and Save, then one box that takes the room left and
 * scrolls, up and down only, should it ever need to. In the box the fields go
 * two to a line and the two switches share the line after; the explanations
 * that came with them went on 16 October 2026 ("remove all that
 * description, i know it because i built it"), so it fits one screen.
 *
 * --user-gap is shared by the fields and the switches, so the phone switch
 * starts exactly where Email address and Quotation history do.
 */
#user-dialog {
    --user-edge: calc(var(--step) * 3);
    --user-gap: calc(var(--step) * 2);
}

#user-dialog[open] {
    /* Held to the window a step below its top, as the roles dialog is. Left
     * where the page put it, under the header and the tabs, a dialog as tall
     * as the window ends below the fold - on a phone, with half of it there. */
    position: fixed;
    inset: calc(var(--step) * 2) 0 auto;
    margin: 0 auto;

    display: flex;
    flex-direction: column;

    /* The box below does the scrolling; one scroll bar, not two. */
    overflow: hidden;

    /* Under the passcode box, over everything else on the page. Held to the
     * window, the dialog now covers the middle of the screen, where the
     * passcode box sits; with the 100 every dialog is given it painted over
     * that box (fixed at 50 - this screen writes it `open` and never
     * reopens it as a modal), so Save, Set PIN and Sign this phone out asked
     * for the passcode and nobody could see where to type it. 40 is still
     * above the header (30), the sticky bar (20) and the tabs (19). */
    z-index: 40;
}

#user-dialog .sheet {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

#user-dialog .sheet-head { flex: none; }

/* Hidden across rather than auto, so nothing in it can ever bring back a
 * sideways scroll bar: what is too wide wraps instead. */
.user-wrap {
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden auto;
    overscroll-behavior: contain;
}

/* Two to a line - name and email, then role and quotation history - each
 * half the dialog, which is wide enough to show "No role yet - can sign in,
 * sees nothing" whole. Set here rather than left to .inputs, whose
 * fit-as-many-as-will-go is what put six in a row. */
/* Everything on one screen (Hussein, 16 October 2026: "make this screen fit
 * into one screen, remove all that description"): two fields to a line, the
 * two switches side by side on the line after, then the PIN as one line. */
#user-dialog .user-fields {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: calc(var(--step) * 1.5) var(--user-gap);
    padding: 16px var(--user-edge) calc(var(--step) * 1.5);
}

/* A switch is half a line - the two of them share one, ruled off from the
 * fields above - each a tick and its name. */
#user-dialog .user-switch {
    display: flex;
    align-items: center;
    padding-top: calc(var(--step) * 1.25);
    border-top: 1px solid var(--dialog-edge);
}

/* The phones they are signed in on, and their PIN: set in from the edges
 * like the fields above them, close under them. */
#user-dialog .phones-signed-in,
#user-dialog .phone-pin {
    margin: 0 var(--user-edge) calc(var(--step) * 1.5);
}

#user-dialog .phones-signed-in h2 {
    margin: 0 0 4px;
    font-size: 0.95rem;
}

#user-dialog .phones-signed-in .sheet-flash,
#user-dialog .phone-pin .sheet-flash { margin: 0 0 6px; }

#user-dialog .user-wrap > .sheet-flash { margin: 16px var(--user-edge) 0; }

/* One to a line below the width where half the dialog would cut the Role
 * dropdown short again; the switches then take a line each. */
@media (max-width: 52rem) {
    #user-dialog .user-fields { grid-template-columns: minmax(0, 1fr); }
}

/* On a phone the dialog is the whole screen, so it starts at the very top,
 * and the title bar comes in to 16 pixels from the edge, so the fields come
 * in with it. */
@media (max-width: 40rem) {
    #user-dialog { --user-edge: 16px; }
    #user-dialog[open] { top: 0; }
}

/* ------------------------------------------------ opening and closing
 *
 * These dialogs are rendered with the `open` attribute from PHP rather than
 * opened by script, so there is no moment to animate from - the page simply
 * arrives with the form already on it, which reads as a jump-cut. The entry
 * animation runs on load instead, which is the same thing to look at.
 *
 * Closing is a navigation: Cancel is a link back to the list. portal.js holds
 * that navigation for the length of the exit animation and then follows it, so
 * the form leaves rather than vanishing.
 *
 * Short and small on purpose. A dialog carrying a permission grid is opened
 * dozens of times in a sitting, and anything longer than about 150ms stops
 * being polish and starts being a wait.
 */
/* Longer and more travel than the first version (Hussein, 3 October 2026:
 * "make the popups come up in transitions instead of just static screen
 * load") - at 150ms and 8px it read as no animation at all. Still short
 * enough never to be waited on.
 *
 * On the individual translate and scale properties, not on transform. The
 * costing-style sheets and the passcode box are centred WITH transform
 * (translate(-50%, -50%)); animating transform replaced that for the length
 * of the animation and the box jumped from the corner. These compose with it. */
@keyframes modal-in {
    from { opacity: 0; translate: 0 18px; scale: 0.97; }
    to   { opacity: 1; translate: 0 0; scale: 1; }
}

@keyframes modal-out {
    from { opacity: 1; translate: 0 0; scale: 1; }
    to   { opacity: 0; translate: 0 10px; scale: 0.985; }
}

@keyframes modal-veil-in {
    from { opacity: 0; }
}

dialog[open] {
    animation: modal-in 280ms cubic-bezier(0.16, 1, 0.3, 1);
}

dialog[open]::backdrop {
    animation: modal-veil-in 280ms ease-out;
}

/* Under 240ms, the time portal.js waits before following the link. */
.modal[open].closing {
    animation: modal-out 170ms cubic-bezier(0.4, 0, 1, 1) forwards;
}

/* The page behind settles back as the form leaves. */
.modal[open] ~ * { transition: opacity 150ms ease; }

@media (prefers-reduced-motion: reduce) {
    dialog[open],
    dialog[open]::backdrop,
    .modal[open].closing {
        animation: none;
    }
}

/* ----------------------------------------------- two lists, side by side
 *
 * Users and roles are read together: putting somebody in a role means looking
 * at the roles, and a role means nothing until you can see who is in it.
 * Stacked, one of them is always off the bottom of the screen while you read
 * the other - which was the problem with them being two pages in the first
 * place, only now with scrolling instead of a link.
 *
 * One column again below 64rem, where two tables of four columns each stop
 * being two tables and start being two columns of wrapped text.
 */
.two-up {
    display: grid;
    /* Not even. The people table carries five columns to the roles table's
     * four, and an email address is the longest thing on either. */
    /* The roles list lost its description column, so it needs much less. */
    grid-template-columns: 1.9fr 1fr;
    gap: calc(var(--step) * 3);
    align-items: start;
    width: 100%;
}

.two-up > section {
    display: flex;
    flex-direction: column;
    gap: calc(var(--step) * 2);
    min-width: 0;
}

@media (max-width: 64rem) {
    .two-up { grid-template-columns: 1fr; }
}

/* Wider than the reading measure, because this screen is two tables rather
 * than a column of prose. 62rem would put five columns of user detail into
 * about 480 pixels. */
.middle.roomy { max-width: 84rem; }

/* In a narrow column the first cell is a name - a person or a role - and a
 * name broken over two lines reads as two things. The description beside it
 * is prose and may wrap as much as it likes. */
.two-up table.rates td:first-child,
.two-up table.rates th:first-child {
    white-space: nowrap;
}

/* Rows the height of one line.
 *
 * The height was never the padding - it was the wrapping. "Last seen" over
 * three lines, "Senior Accountant" over two, a role's description over three:
 * every one of them set the height of its whole row to say something short.
 *
 * So nothing in these two lists wraps. What does not fit scrolls inside the
 * table's own box, which is what .table-scroll is for, and a name stays one
 * name.
 */
.two-up table.rates td,
.two-up table.rates th {
    white-space: nowrap;
    padding-top: calc(var(--step) * 0.8);
    padding-bottom: calc(var(--step) * 0.8);
    font-size: 0.875rem;
}

/* The dates are the only thing here set in the data face, and at 0.875rem it
 * runs wider than the sans. */
.two-up table.rates td.data { font-size: 0.8125rem; }

/* The flight list, centred column by column.
 *
 * Scoped to .flights on purpose. The convention everywhere else is that text
 * reads down a column from its first letter, which is right for a customer or
 * a person; none of the five columns here is a name. A serial, a date, a
 * badge, a registration and a button are all much the same width row to row,
 * and centred they sit under their headings instead of drifting away from
 * them.
 *
 * The access grid borrows this table's density but not its centring: its
 * first column is a list of names, which is exactly the case the rule above
 * excludes. Its tick columns are centred on their own further down. */
table.flights:not(.access) th,
table.flights:not(.access) td { text-align: center; }

/* The month a run of briefings belongs to.
 *
 * Left, not centred like the columns: this one is a label for everything
 * below it rather than the top of a column, and a heading centred across
 * seven columns reads as a title for the table instead of a break within it.
 * It is a th with scope=rowgroup, so a screen reader announces the month
 * before the rows it covers. */
.flights tbody tr.month-break th {
    padding: calc(var(--step) * 1.5) calc(var(--step) * 1.6) calc(var(--step) * 0.75);
    text-align: left;
    font-family: var(--font-display);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-faint);
    background: var(--bg);
    border-bottom: 1px solid var(--border);
}

/* The first one carries no line above it - there is nothing above it to
 * separate from, and a rule directly under the table head reads as a mistake. */
.flights tbody tr.month-break:first-child th { padding-top: calc(var(--step) * 0.75); }

/* How many flights the month holds, beside its name.
 *
 * A count, not a label, so it is set in the figure font and kept quiet - the
 * month is what somebody is scanning for and the number is what they want
 * once they have found it. */
.flights tbody tr.month-break .count {
    display: inline-block;
    margin-left: calc(var(--step) * 0.75);
    padding: 0 calc(var(--step) * 0.9);
    border-radius: 999px;
    background: var(--surface-2, var(--surface));
    border: var(--edge);
    font-family: var(--font-data);
    font-size: 0.6875rem;
    letter-spacing: 0;
    color: var(--text-faint);
}

/* ------------------------------------------------- flights validation
 *
 * The costing list borrows the flight list's density and its month bands.
 * What it does not borrow is centring every column: this table has a
 * customer on it, and a name reads from its first letter. Raised above
 * table.flights on purpose rather than left to source order, because the
 * two rules would otherwise be the same weight and the winner would be
 * whichever happened to be written last. */
table.flights.costing th.name,
table.flights.costing td.name { text-align: left; }

/* Where a flight has got to. Four chips rather than four words, because the
 * column is read down rather than across - the eye is looking for the ones
 * that are not like the others. */
.role.costing-not_costed {
    background: rgb(148 163 184 / 0.16);
    color: var(--text-faint);
}

.role.costing-costed {
    background: rgb(250 176 5 / 0.16);
    border: 1px solid rgb(250 176 5 / 0.35);
    color: #f0a020;
}

.role.costing-validated {
    background: rgb(64 192 87 / 0.16);
    border: 1px solid rgb(64 192 87 / 0.35);
    color: #51cf66;
}

.role.costing-invoiced {
    background: rgb(108 92 231 / 0.18);
    border: 1px solid rgb(108 92 231 / 0.4);
    color: #a99cf5;
}

/* ------------------------------------------------- the costing sheet
 *
 * Read beside a photograph of the briefing, because that is what somebody
 * using it is doing: holding a carbon-pad ACCOUNTS column against a rate card
 * and checking one against the other.
 *
 * So this is a sheet, not a form. Figures are ruled fields rather than boxes,
 * and every amount sits in one unbroken column running from the first line
 * down to NET, so the eye can travel down it the way it travels down the pad.
 * The first version gave a three-digit quantity three hundred pixels of
 * pill-shaped input and truncated the description to "Charter cost - Ces".
 */

.modal.sheet-modal {
    width: min(58rem, calc(100vw - var(--step) * 3));
    /* Centred on the screen, not on the document.
     *
     * A dialog rendered with the open attribute is non-modal, and browsers
     * give those position:absolute with margin:auto - which centres them in
     * the containing block. On a list of a hundred and twenty-five briefings
     * that block is four thousand pixels tall, so the sheet settled halfway
     * down it and the screen looked broken. */
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    margin: 0;
    max-height: calc(100dvh - var(--step) * 3);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.modal.sheet-modal .sheet.costing {
    padding: 0;
    overflow: auto;
    flex: 1;
}

/* ---------------------------------------------------------- the head */

.costing-head {
    position: sticky;
    top: 0;
    z-index: 3;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--step);
    flex-wrap: wrap;
    padding: calc(var(--step) * 1.4) calc(var(--step) * 1.8);
    background: var(--bg);
    border-bottom: 1px solid var(--border);
}

.costing-who { display: flex; align-items: center; flex-wrap: wrap; gap: 0.6em; }

.costing-who h1 {
    margin: 0;
    font-family: var(--font-data);
    font-size: 1.35rem;
    font-weight: 600;
    letter-spacing: 0.02em;
}

/* The whole identity of the flight on one line under the serial, so the head
 * costs two lines rather than five. */
.costing-who p {
    flex-basis: 100%;
    margin: 0.15em 0 0;
    font-size: 0.75rem;
    color: var(--text-dim);
}

.costing-who p i { color: var(--text-faint); font-style: normal; padding: 0 0.15em; }

.costing-acts { display: flex; align-items: center; gap: calc(var(--step) * 0.6); }

/* ------------------------------------------------------- the notices */

/* A rule and a line of text. The old banner was a filled panel three lines
 * tall for one sentence, which made a note about missing miles look like a
 * failure. */
.costing-note {
    margin: 0;
    padding: calc(var(--step) * 0.8) calc(var(--step) * 1.8);
    border-left: 2px solid var(--text-faint);
    background: var(--surface);
    font-size: 0.8125rem;
    color: var(--text-dim);
}

.costing-note.alarm { border-left-color: var(--alarm); color: var(--text); }

/* --------------------------------------------------------- the route */

/* The debriefing, row for row. Sits at the head of the sheet because
 * everything below it - the miles, the landings, the levies - is worked out
 * from what is typed here. */
.costing-route {
    padding: calc(var(--step) * 1.1) calc(var(--step) * 1.8) calc(var(--step) * 0.9);
    border-bottom: 1px solid var(--border);
}

table.route-legs { border-collapse: collapse; margin: 0; }

table.route-legs th {
    padding: 0 calc(var(--step) * 0.5) calc(var(--step) * 0.3) 0;
    font-family: var(--font-display);
    font-size: 0.5625rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--text-faint);
    text-align: left;
}

table.route-legs td { padding: 0.15em calc(var(--step) * 0.5) 0.15em 0; }

/* Ruled like the figures row: a line to write on. */
table.route-legs input {
    padding: 0.25em 0.4em;
    border: 0;
    border-bottom: 2px solid var(--border);
    border-radius: 3px 3px 0 0;
    background: var(--surface);
    color: var(--text);
    font-family: var(--font-data);
    font-size: 0.875rem;
    font-variant-numeric: tabular-nums;
}

table.route-legs input[name$="[from]"],
table.route-legs input[name$="[to]"] { width: 6.5rem; text-transform: uppercase; }
table.route-legs input[type="number"] { width: 3.5rem; text-align: right; }
table.route-legs input[type="date"] { width: 9.5rem; }

table.route-legs input:hover { border-bottom-color: var(--text-faint); }

table.route-legs input:focus {
    outline: none;
    border-bottom-color: var(--accent, #6c5ce7);
}

table.route-legs input[readonly] { background: none; border-bottom-color: transparent; }

.leg-remove {
    padding: 0 0.35em;
    border: 0;
    background: none;
    color: var(--text-faint);
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
}

.leg-remove:hover { color: var(--alarm); }

.route-foot {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--step);
    margin-top: calc(var(--step) * 0.6);
}

/* What the chart made of the route. Read-only, and quiet: it is the answer,
 * not something to edit. */
.route-sum {
    margin: 0;
    font-family: var(--font-data);
    font-size: 0.75rem;
    color: var(--text-dim);
}

.route-sum i { font-style: normal; color: var(--text-faint); padding: 0 0.2em; }

/* The extra-charge row: a night stop or a permit, typed in full. A faint rule
 * above it separates what was typed from what was priced. */
table.costing-lines tr.extra td { border-top: 1px dashed var(--border); }

/* ------------------------------------------------------- the figures */

/* One row: VAT and discount. Everything else the sheet used to ask for here
 * is now worked out from the route above it. */
.costing-figures {
    display: flex;
    flex-wrap: wrap;
    gap: calc(var(--step) * 1.6);
    padding: calc(var(--step) * 1.1) calc(var(--step) * 1.8) calc(var(--step) * 1.5);
    border-bottom: 1px solid var(--border);
}

.costing-figures label {
    display: flex;
    align-items: baseline;
    gap: 0.5em;
    position: relative;
}

.costing-figures label > span {
    font-family: var(--font-display);
    font-size: 0.625rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-faint);
    white-space: nowrap;
}

.costing-figures label > i { font-style: normal; color: var(--text-faint); }

/* A ruled field, the way a form on paper is ruled: a line to write on, not a
 * box to fill. */
.costing-figures input {
    width: 4.5rem;
    padding: 0.25em 0.4em;
    border: 0;
    /* Two solid rules rather than one hairline. Empty
     * with a single faint underline they read as decoration - the first
     * version of this looked like a row of labels with nothing under them. */
    border-bottom: 2px solid var(--border);
    border-radius: 3px 3px 0 0;
    background: var(--surface);
    color: var(--text);
    font-family: var(--font-data);
    font-size: 0.9375rem;
    font-variant-numeric: tabular-nums;
    text-align: right;
}

.costing-figures input:hover { border-bottom-color: var(--text-faint); }

/* Empty is the state that needs the prompt, so it gets the accent rather
 * than the filled ones. Nothing is wrong with it - it is simply the next
 * thing to do. */
.costing-figures input:placeholder-shown,
.costing-figures input:not([value]):not(:focus),
.costing-figures input[value=""]:not(:focus) {
    border-bottom-style: dashed;
    border-bottom-color: var(--text-faint);
}

.costing-figures input:focus {
    outline: none;
    border-bottom-color: var(--accent, #6c5ce7);
    background: var(--surface);
}

/* The Create Invoice popup borrows the costing sheet. Its head fields are
 * wider than the sheet's two percentages: a Tally number, a date, a currency
 * and a rate. */
.invoice-fields label.wide input { width: 11rem; text-align: left; }
.invoice-fields label.date input { width: 9.5rem; text-align: left; }
.invoice-fields input[name="rate"] { width: 8rem; }

.invoice-fields select {
    padding: 0.25em 0.4em;
    border: 0;
    border-bottom: 2px solid var(--border);
    border-radius: 3px 3px 0 0;
    background: var(--surface);
    color: var(--text);
    font-family: var(--font-data);
    font-size: 0.9375rem;
}

table.invoice-pick td.l label { cursor: pointer; }
table.invoice-pick td.faint { color: var(--text-faint); }

/* The search over the unbilled briefings, and what is ticked so far. */
.pick-bar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--step);
    padding: var(--step) calc(var(--step) * 1.8);
    border-bottom: 1px solid var(--border);
}

.pick-search {
    flex: 1 1 18rem;
    padding: 0.45em 0.7em;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
    color: var(--text);
    font-size: 0.875rem;
}

.pick-search:focus { outline: none; border-color: var(--accent, #6c5ce7); }

.pick-billed {
    display: inline-flex;
    align-items: center;
    gap: 0.4em;
    font-size: 0.75rem;
    color: var(--text-dim);
    cursor: pointer;
    white-space: nowrap;
}

/* Which invoice a briefing is already on, beside its customer. */
.pick-on {
    margin-left: 0.5em;
    font-family: var(--font-data);
    font-size: 0.6875rem;
    color: var(--text-faint);
}

table.invoice-pick tr.billed td { color: var(--text-dim); }

.pick-summary {
    font-family: var(--font-data);
    font-size: 0.75rem;
    color: var(--text-dim);
}

/* The list scrolls inside the popup; the number, the search and Save stay put. */
.pick-scroll { max-height: min(55vh, 34rem); overflow-y: auto; }

/* Another customer's flight, once one customer is ticked. Still readable -
 * it is still waiting to be billed - but plainly not part of this invoice. */
table.invoice-pick tr.other-customer td { opacity: 0.35; }

/* --------------------------------------------------------- the lines */

table.costing-lines {
    width: 100%;
    border-collapse: collapse;
    margin: 0;
}

table.costing-lines th,
table.costing-lines td {
    padding: calc(var(--step) * 0.35) calc(var(--step) * 0.5);
    border: 0;
    vertical-align: middle;
}

table.costing-lines thead th {
    padding-top: calc(var(--step) * 1.1);
    font-family: var(--font-display);
    font-size: 0.5625rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--text-faint);
    border-bottom: 1px solid var(--border);
}

table.costing-lines .l { text-align: left; padding-left: calc(var(--step) * 1.8); }
/* Wide enough that "410  statute miles" stays on one line. At 9.5rem the
 * unit wrapped under the number and the quantity column grew a second row
 * of its own. */
table.costing-lines .q { text-align: right; width: 11.5rem; white-space: nowrap; }
table.costing-lines .r { text-align: right; width: 6rem; }
table.costing-lines .a { text-align: right; width: 8rem; }
table.costing-lines .v { text-align: center; width: 3rem; padding-right: calc(var(--step) * 1.8); }

table.costing-lines tbody tr:hover { background: var(--surface); }

table.costing-lines input[type="text"],
table.costing-lines input[type="number"] {
    padding: 0.2em 0.25em;
    border: 0;
    border-bottom: 1px solid transparent;
    border-radius: 0;
    background: none;
    color: var(--text);
    font-family: var(--font-data);
    font-size: 0.8125rem;
    font-variant-numeric: tabular-nums;
}

table.costing-lines td.l input { width: 100%; text-align: left; }

/* Quantity and its unit read as one thing - "410 statute miles" - because
 * that is how the pad reads. The unit is quieter than the number. */
table.costing-lines td.q input[data-qty] { width: 3.5rem; text-align: right; }

table.costing-lines td.q input.unit {
    width: 5.5rem;
    text-align: left;
    color: var(--text-faint);
    font-size: 0.6875rem;
}

table.costing-lines td.r input { width: 100%; text-align: right; }

/* The amount column. Slightly heavier than the rest of the row, because this
 * is the figure the row is about. */
table.costing-lines td.a input {
    width: 100%;
    text-align: right;
    font-size: 0.875rem;
    font-weight: 600;
}

table.costing-lines input:hover { border-bottom-color: var(--border); }

table.costing-lines input:focus {
    outline: none;
    border-bottom-color: var(--accent, #6c5ce7);
    background: var(--surface);
}

/* A figure somebody typed over the rate card's answer. Marked rather than
 * refused: the briefing is the authority and its arithmetic is sometimes not
 * quantity times rate. But it should be visible which ones those are. */
table.costing-lines tr.typed td.a input {
    color: #f0a020;
    border-bottom-color: rgb(250 176 5 / 0.5);
}

/* -------------------------------------------------------- the totals */

table.costing-lines tfoot th {
    text-align: right;
    padding-right: calc(var(--step) * 1.2);
    font-family: var(--font-display);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-dim);
}

table.costing-lines tfoot td.a {
    font-family: var(--font-data);
    font-size: 0.875rem;
    font-variant-numeric: tabular-nums;
    color: var(--text);
    /* Padded to sit under the amount inputs above, which carry their own
     * quarter-em, so the column stays a column. */
    padding-right: calc(var(--step) * 0.75);
}

table.costing-lines tfoot tr.rule th,
table.costing-lines tfoot tr.rule td {
    border-top: 1px solid var(--border);
    padding-top: calc(var(--step) * 0.7);
}

table.costing-lines tfoot tr.quiet th,
table.costing-lines tfoot tr.quiet td { color: var(--text-faint); font-weight: 400; }

/* Doubled, the way a ledger closes a column. */
table.costing-lines tfoot tr.net th,
table.costing-lines tfoot tr.net td {
    border-top: 3px double var(--border);
    padding-top: calc(var(--step) * 0.6);
    padding-bottom: calc(var(--step) * 1.4);
    font-size: 1.0625rem;
    font-weight: 700;
    color: var(--text);
}

/* ---------------------------------------------------------- the foot */

.costing-foot {
    position: sticky;
    bottom: 0;
    display: flex;
    align-items: center;
    gap: var(--step);
    flex-wrap: wrap;
    padding: var(--step) calc(var(--step) * 1.8);
    border-top: 1px solid var(--border);
    background: var(--bg);
}

.costing-foot form { display: flex; gap: calc(var(--step) * 0.6); align-items: center; margin: 0; }

.costing-foot .by {
    margin: 0;
    flex: 1;
    font-size: 0.6875rem;
    color: var(--text-faint);
}

.costing-foot .taking-back input[type="text"] {
    width: 14rem;
    padding: 0.35em 0.6em;
    font-size: 0.75rem;
}

@media (max-width: 40rem) {
    table.costing-lines .l { padding-left: var(--step); }
    table.costing-lines .v { padding-right: var(--step); }
    .costing-figures { gap: var(--step); }
    .costing-figures input { width: 3.5rem; }
    .costing-route { padding-left: var(--step); padding-right: var(--step); overflow-x: auto; }
    table.route-legs input[name$="[from]"],
    table.route-legs input[name$="[to]"] { width: 4.5rem; }
    table.route-legs input[type="date"] { width: 8rem; }
}
.sheet-foot .inline { display: flex; gap: var(--step); align-items: center; margin: 0; }

/* The briefing, over the form.
 *
 * Fixed to the viewport and above the dialog, so the form underneath keeps
 * everything typed into it. Closing this reveals the form exactly as it was;
 * navigating to the document instead would have thrown the typing away. */
/* [hidden] is an attribute, and the display:flex below would beat the
 * browser's own display:none for it. Said explicitly, because a scan layer
 * that ignores hidden covers the very form it is meant to sit beside. */
.scan-layer[hidden] { display: none; }

.scan-layer {
    position: fixed;
    inset: 0;
    /* Above .modal[open], which is 100. The scan is opened from inside the
     * dialog and has to cover it; at 60 it opened underneath the very form it
     * was summoned from and only the margins of it were visible. */
    z-index: 200;
    display: flex;
    flex-direction: column;
    /* --bg, not --surface. The surface tokens are translucent - they are
     * meant to sit over something - so the layer was correctly on top and
     * completely see-through, and the form underneath read straight through
     * the scan. The dialog uses --bg for the same reason. */
    background: var(--bg, #12131a);
}

.scan-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--step);
    padding: calc(var(--step) * 1.2) calc(var(--step) * 2);
    border-bottom: 1px solid var(--border);
}

.scan-layer iframe {
    flex: 1;
    width: 100%;
    border: 0;
}

/* ================================================================ controls
 *
 * One button system for the whole portal, and the header's person menu.
 * Hussein, 3 October 2026: "make the buttons more cool man, they look like
 * the 19th century futuristic buttons ... i don't want the website to look
 * AI made at all". Navy from the logo instead of the stock violet, solid
 * lit keys instead of outlined boxes, sentence case instead of spaced
 * monospace capitals.
 *
 * Solid fills and a light from above: a faint highlight on the top edge,
 * one pixel of shadow under the bottom one, and a one-pixel press. That is
 * the difference between a control you can press and a drawing of one.
 * Navy from the logo is the primary in daylight; on the dark theme the
 * primary inverts to paper, because navy on navy is not a button.
 *
 * Sentence case in the system face. Monospace stays with the figures,
 * where it earns its keep, and leaves the controls alone.
 */

:root {
    /* The face the operating system draws its own buttons in. Outfit was
     * named in --font-body but never loaded, and the CSP (font-src 'self')
     * would refuse Google Fonts anyway. Biome replaces this the day its web
     * licence lands. */
    --font-ui: 'Segoe UI Variable Text', 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', Arial, sans-serif;
    --btn-radius: 6px;
    --control-h: 36px;
    --control-h-sm: 30px;
}

/* Dark - the default. */
:root,
:root[data-theme='dark'] {
    --focus: #9db0ff;

    --btn-bg: #1d2350;
    --btn-bg-hover: #252c5c;
    --btn-bg-active: #181d42;
    --btn-border: rgb(255 255 255 / 0.11);
    --btn-border-hover: rgb(255 255 255 / 0.18);
    --btn-text: #e6e8f3;
    --btn-shadow: inset 0 1px 0 rgb(255 255 255 / 0.06), 0 1px 0 rgb(0 0 0 / 0.45);

    --btn-primary-bg: #eef0f8;
    --btn-primary-bg-hover: #ffffff;
    --btn-primary-bg-active: #dde0ec;
    --btn-primary-border: #ffffff;
    --btn-primary-text: #0b0f2b;
    --btn-primary-shadow: inset 0 -1px 0 rgb(11 15 43 / 0.22), 0 1px 0 rgb(0 0 0 / 0.5);

    --btn-ghost-bg: rgb(255 255 255 / 0.05);
    --btn-ghost-bg-hover: rgb(255 255 255 / 0.09);
    --btn-ghost-text: #b4b9d6;

    --btn-danger-bg: #c9302c;
    --btn-danger-bg-hover: #d63a35;
    --btn-danger-border: #a3221e;
    --btn-danger-text: #ffffff;
    --btn-danger-ink: #ff8f85;
    --btn-danger-wash: rgb(255 90 78 / 0.10);
    --btn-danger-wash-hover: rgb(255 90 78 / 0.18);

    --seg-track: rgb(255 255 255 / 0.05);
    --seg-track-border: rgb(255 255 255 / 0.07);
    --seg-on: #262c57;
    --seg-on-shadow: inset 0 1px 0 rgb(255 255 255 / 0.07), 0 1px 0 rgb(0 0 0 / 0.4);

    --avatar-bg: #2e3a8f;
    --avatar-text: #ffffff;

    --menu-bg: #141938;
    --menu-border: rgb(255 255 255 / 0.10);
    --menu-hover: rgb(255 255 255 / 0.06);
    --menu-shadow: inset 0 1px 0 rgb(255 255 255 / 0.04), 0 2px 6px rgb(0 0 0 / 0.3), 0 18px 40px -12px rgb(0 0 0 / 0.7);
}

/* Light, chosen. */
:root[data-theme='light'] {
    --focus: #2d4bd1;

    --btn-bg: #ffffff;
    --btn-bg-hover: #f7f8fc;
    --btn-bg-active: #eef0f6;
    --btn-border: #cfd3e2;
    --btn-border-hover: #b6bbd0;
    --btn-text: #1a1f45;
    --btn-shadow: 0 1px 0 rgb(19 23 53 / 0.07), 0 1px 2px rgb(19 23 53 / 0.05);

    --btn-primary-bg: #202b7d;
    --btn-primary-bg-hover: #283591;
    --btn-primary-bg-active: #192262;
    --btn-primary-border: #141b57;
    --btn-primary-text: #ffffff;
    --btn-primary-shadow: inset 0 1px 0 rgb(255 255 255 / 0.14), 0 1px 0 rgb(11 15 43 / 0.25), 0 1px 3px rgb(11 15 43 / 0.12);

    --btn-ghost-bg: rgb(25 34 98 / 0.05);
    --btn-ghost-bg-hover: rgb(25 34 98 / 0.09);
    --btn-ghost-text: #3f4670;

    --btn-danger-bg: #b42318;
    --btn-danger-bg-hover: #9f1f15;
    --btn-danger-border: #8f1b12;
    --btn-danger-text: #ffffff;
    --btn-danger-ink: #b42318;
    --btn-danger-wash: rgb(180 35 24 / 0.06);
    --btn-danger-wash-hover: rgb(180 35 24 / 0.12);

    --seg-track: #e7e9f2;
    --seg-track-border: rgb(19 23 53 / 0.06);
    --seg-on: #ffffff;
    --seg-on-shadow: 0 0 0 1px rgb(19 23 53 / 0.08), 0 1px 2px rgb(19 23 53 / 0.12);

    --avatar-bg: #192262;
    --avatar-text: #ffffff;

    --menu-bg: #ffffff;
    --menu-border: rgb(19 23 53 / 0.12);
    --menu-hover: #f1f2f8;
    --menu-shadow: 0 2px 6px rgb(19 23 53 / 0.06), 0 18px 36px -12px rgb(19 23 53 / 0.28);
}

/* Light, by way of Auto on a light desktop. The same values - leaving these
 * out is how Auto users once got the dark theme's colours on white paper. */
@media (prefers-color-scheme: light) {
    :root:not([data-theme]) {
        --focus: #2d4bd1;

        --btn-bg: #ffffff;
        --btn-bg-hover: #f7f8fc;
        --btn-bg-active: #eef0f6;
        --btn-border: #cfd3e2;
        --btn-border-hover: #b6bbd0;
        --btn-text: #1a1f45;
        --btn-shadow: 0 1px 0 rgb(19 23 53 / 0.07), 0 1px 2px rgb(19 23 53 / 0.05);

        --btn-primary-bg: #202b7d;
        --btn-primary-bg-hover: #283591;
        --btn-primary-bg-active: #192262;
        --btn-primary-border: #141b57;
        --btn-primary-text: #ffffff;
        --btn-primary-shadow: inset 0 1px 0 rgb(255 255 255 / 0.14), 0 1px 0 rgb(11 15 43 / 0.25), 0 1px 3px rgb(11 15 43 / 0.12);

        --btn-ghost-bg: rgb(25 34 98 / 0.05);
        --btn-ghost-bg-hover: rgb(25 34 98 / 0.09);
        --btn-ghost-text: #3f4670;

        --btn-danger-bg: #b42318;
        --btn-danger-bg-hover: #9f1f15;
        --btn-danger-border: #8f1b12;
        --btn-danger-text: #ffffff;
        --btn-danger-ink: #b42318;
        --btn-danger-wash: rgb(180 35 24 / 0.06);
        --btn-danger-wash-hover: rgb(180 35 24 / 0.12);

        --seg-track: #e7e9f2;
        --seg-track-border: rgb(19 23 53 / 0.06);
        --seg-on: #ffffff;
        --seg-on-shadow: 0 0 0 1px rgb(19 23 53 / 0.08), 0 1px 2px rgb(19 23 53 / 0.12);

        --avatar-bg: #192262;
        --avatar-text: #ffffff;

        --menu-bg: #ffffff;
        --menu-border: rgb(19 23 53 / 0.12);
        --menu-hover: #f1f2f8;
        --menu-shadow: 0 2px 6px rgb(19 23 53 / 0.06), 0 18px 36px -12px rgb(19 23 53 / 0.28);
    }
}

/* ------------------------------------------------------------------ button
 *
 * The base .action IS the secondary button, so a bare class="action" (the
 * rates Save, the no-access Back) is a button and not loose text. .quiet
 * stays as its name. Variants only swap the local --_ values. */

.action {
    --_bg: var(--btn-bg);
    --_bg-hover: var(--btn-bg-hover);
    --_bg-active: var(--btn-bg-active);
    --_border: var(--btn-border);
    --_border-hover: var(--btn-border-hover);
    --_text: var(--btn-text);
    --_shadow: var(--btn-shadow);

    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    min-height: var(--control-h);
    padding: 0 14px;
    border: 1px solid var(--_border);
    border-radius: var(--btn-radius);
    background: var(--_bg);
    box-shadow: var(--_shadow);
    color: var(--_text);
    font-family: var(--font-ui);
    font-size: 0.875rem;
    font-weight: 550;
    line-height: 1.2;
    letter-spacing: -0.005em;
    text-decoration: none;
    cursor: pointer;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    transition:
        background-color 120ms ease,
        border-color 120ms ease,
        box-shadow 120ms ease,
        color 120ms ease,
        transform 60ms ease;
}

.action:hover {
    background: var(--_bg-hover);
    border-color: var(--_border-hover);
    color: var(--_text);
}

/* Pressed: down one pixel and the shadow goes, as a key does. */
.action:active {
    background: var(--_bg-active);
    box-shadow: none;
    transform: translateY(1px);
}

/* The global :focus-visible also sets border-radius: 4px, which squared off
 * every focused button. Said again here. */
.action:focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: 2px;
    border-radius: var(--btn-radius);
}

.action > svg,
.action > .icon {
    flex: none;
    width: 16px;
    height: 16px;
    margin-inline-start: -2px;
    opacity: 0.78;
    transition: opacity 120ms ease;
}

.action:hover > svg,
.action:hover > .icon { opacity: 1; }

/* Primary: one per screen. */
.action.primary {
    --_bg: var(--btn-primary-bg);
    --_bg-hover: var(--btn-primary-bg-hover);
    --_bg-active: var(--btn-primary-bg-active);
    --_border: var(--btn-primary-border);
    --_border-hover: var(--btn-primary-border);
    --_text: var(--btn-primary-text);
    --_shadow: var(--btn-primary-shadow);
}

.action.primary > svg,
.action.primary > .icon { opacity: 1; }

/* Ghost: the row-level button. Smaller, borderless, but still a filled
 * shape - a ghost with no fill is how "Back" once became invisible text. */
.action.ghost {
    --_bg: var(--btn-ghost-bg);
    --_bg-hover: var(--btn-ghost-bg-hover);
    --_bg-active: var(--btn-ghost-bg-hover);
    --_border: transparent;
    --_border-hover: transparent;
    --_text: var(--btn-ghost-text);
    --_shadow: none;

    min-height: var(--control-h-sm);
    padding: 0 10px;
    font-size: 0.8125rem;
    font-weight: 500;
}

.action.ghost:hover { --_text: var(--text); }

/* Danger: for the confirmations that cannot be taken back in one click -
 * cancel a flight, retire an airstrip. Red keeps meaning only that. */
.action.danger {
    --_bg: var(--btn-danger-bg);
    --_bg-hover: var(--btn-danger-bg-hover);
    --_bg-active: var(--btn-danger-border);
    --_border: var(--btn-danger-border);
    --_border-hover: var(--btn-danger-border);
    --_text: var(--btn-danger-text);
    --_shadow: inset 0 1px 0 rgb(255 255 255 / 0.12), 0 1px 0 rgb(0 0 0 / 0.25);
}

.action.ghost.danger {
    --_bg: var(--btn-danger-wash);
    --_bg-hover: var(--btn-danger-wash-hover);
    --_bg-active: var(--btn-danger-wash-hover);
    --_border: transparent;
    --_border-hover: transparent;
    --_text: var(--btn-danger-ink);
    --_shadow: none;
}

.action.ghost.danger:hover { --_text: var(--btn-danger-ink); }

/* Disabled, whether by attribute (passcode Confirm) or by aria (preview). */
.action:disabled,
.action[aria-disabled='true'] {
    opacity: 0.5;
    box-shadow: none;
    transform: none;
    cursor: not-allowed;
    pointer-events: none;
}

/* Buttons side by side are one height, ghost included. */
.bar .action,
.head-right .action {
    min-height: var(--control-h);
    padding: 0 14px;
    font-size: 0.875rem;
}

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

.shell > header.topbar {
    position: relative;
    z-index: 30; /* the person menu opens over the sticky .bar (20); dialogs are 100 */
    align-items: center;
    padding-bottom: calc(var(--step) * 2);
    border-bottom: 1px solid var(--border);
}

.topbar-tools {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
}

/* Home, in the header on every screen but the home page. The same key as
 * any secondary button, a little shorter to sit level with the theme switch. */
.topbar-home {
    flex: none;
    min-height: 34px;
    padding: 0 12px;
}

/* ------------------------------------------------------------ theme toggle
 *
 * A segmented control: a recessed track, the chosen one raised like a key. */

.theme {
    display: inline-flex;
    gap: 2px;
    padding: 2px;
    border: 1px solid var(--seg-track-border);
    border-radius: 8px; /* 6 inside + 2 padding, so the corners nest */
    background: var(--seg-track);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

.theme button {
    min-width: 0;
    min-height: 28px;
    padding: 0 11px;
    border: 0;
    border-radius: var(--btn-radius);
    background: transparent;
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    font-weight: 500;
    letter-spacing: 0;
    text-transform: none;
    cursor: pointer;
    transition: background-color 120ms ease, color 120ms ease, box-shadow 120ms ease;
}

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

.theme button[aria-pressed='true'] {
    background: var(--seg-on);
    color: var(--text);
    box-shadow: var(--seg-on-shadow);
}

.theme button:focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: 1px;
    border-radius: var(--btn-radius);
}

/* ------------------------------------------------------------- person menu */

.user-menu {
    position: relative;
    margin-left: 2px;
    padding-left: 12px;
    border-left: 1px solid var(--border);
}

.user-chip {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: var(--control-h);
    padding: 3px 8px 3px 3px;
    border: 1px solid transparent;
    border-radius: var(--btn-radius);
    color: var(--text);
    font-family: var(--font-ui);
    list-style: none;
    cursor: pointer;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    transition: background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
}

.user-chip::-webkit-details-marker { display: none; }
.user-chip::marker { content: ''; }

.user-chip:hover { background: var(--btn-ghost-bg-hover); }

.user-menu[open] > .user-chip {
    background: var(--btn-bg);
    border-color: var(--btn-border);
    box-shadow: var(--btn-shadow);
}

.user-chip:focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: 2px;
    border-radius: var(--btn-radius);
}

/* Square, like a crew badge - not another round bubble. */
.avatar {
    display: grid;
    place-items: center;
    flex: none;
    width: 28px;
    height: 28px;
    border-radius: 5px;
    background: var(--avatar-bg);
    color: var(--avatar-text);
    font-size: 0.75rem;
    font-weight: 650;
    letter-spacing: 0.02em;
    line-height: 1;
    box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08), inset 0 -1px 0 rgb(0 0 0 / 0.25);
}

.user-id {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
    line-height: 1.15;
    text-align: left;
}

.user-name {
    font-size: 0.875rem;
    font-weight: 600;
    letter-spacing: -0.005em;
    color: var(--text);
    white-space: nowrap;
}

.user-role {
    font-size: 0.75rem;
    color: var(--text-dim);
    white-space: nowrap;
}

/* icon('chevron') points right; turned to point down, and up when open. */
.user-chip > .icon {
    flex: none;
    width: 14px;
    height: 14px;
    color: var(--text-dim);
    transform: rotate(90deg);
    transition: transform 160ms ease;
}

.user-menu[open] > .user-chip > .icon { transform: rotate(-90deg); }

.user-pop {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    z-index: 60;
    width: 16.5rem;
    max-width: calc(100vw - 2rem);
    padding: 6px;
    border: 1px solid var(--menu-border);
    border-radius: 8px;
    background: var(--menu-bg);
    box-shadow: var(--menu-shadow);
    font-family: var(--font-ui);
    transform-origin: top right;
}

@media (prefers-reduced-motion: no-preference) {
    .user-menu[open] > .user-pop { animation: user-pop-in 120ms ease-out; }
}

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

.user-head {
    display: grid;
    gap: 2px;
    padding: 8px 10px 10px;
}

.user-head strong {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--text);
}

.user-head span {
    font-size: 0.8125rem;
    color: var(--text-dim);
    overflow-wrap: anywhere;
}

.user-head .user-tag {
    justify-self: start;
    margin-top: 6px;
    padding: 2px 6px;
    border: 1px solid var(--btn-border);
    border-radius: 4px;
    font-size: 0.6875rem;
    font-weight: 600;
    color: var(--text-dim);
}

.user-list {
    display: grid;
    gap: 1px;
    margin: 0 -6px;
    padding: 6px 6px 0;
    border-top: 1px solid var(--border);
    list-style: none;
}

.user-item {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 34px;
    padding: 0 10px;
    border-radius: 5px;
    color: var(--text);
    font-size: 0.875rem;
    font-weight: 500;
    text-decoration: none;
}

.user-item > .icon {
    flex: none;
    width: 16px;
    height: 16px;
    color: var(--text-dim);
}

.user-item:hover,
.user-item:focus-visible {
    background: var(--menu-hover);
}

.user-item:focus-visible {
    outline: none;
    box-shadow: inset 0 0 0 2px var(--focus);
}

.user-item[aria-disabled='true'] {
    color: var(--text-dim);
    cursor: not-allowed;
}

.user-item[aria-disabled='true']:hover { background: none; }

.user-item em {
    margin-left: auto;
    font-style: normal;
    font-size: 0.75rem;
    color: var(--text-dim);
}

/* ------------------------------------------------------------- on a phone
 *
 * Measured: mark 86 + switch 138 + badge 59 + gaps = 297px at 375 wide
 * (343 usable); 261px at 320 with the smaller rules below.
 *
 * Home and the clear-screen button joined the row since, 74 pixels between
 * them, and the row stopped fitting: on a phone the header was three rows -
 * the mark, then the buttons on a line of their own, then the screen's name
 * - 137 pixels before the page began (measured 22 October 2026, putting the
 * Flights screen right on a phone; Hussein: "i don't like the way the UI of
 * the flights is coming on the phone, its very ugly"). A slightly smaller
 * mark, the theme buttons 2 pixels narrower a side and the badge without its
 * arrow (the badge itself is the button) make it one row again: 324 pixels
 * at 375, 320 at 360. Under 360, see the 22.5rem rules below. */

/* Between a phone and a laptop - a tablet, a narrow window - the header's row
 * is about 840 pixels with the name in the badge, so from 769 to about 860 it
 * ran 12 to 27 pixels past the right edge and every screen slid sideways by
 * that much (found 26 October 2026, at 800). The badge keeps its initials and
 * the name goes, as it already does below 48rem; the menu still says who. */
@media (max-width: 56rem) {
    .user-id {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .user-chip {
        gap: 6px;
        padding-right: 6px;
    }
}

/* Up to 1008 where the header carries a screen's name: from 897, with the
 * name back beside the mark, the name in the badge left it too little room
 * (28 October 2026), and from 961, where the badge's name came back, it cut
 * Engineering / Consignments Tracking by 41 pixels and still by 2 at 1000.
 * Only there: Home has no name beside the mark, and showed only HR from 897
 * to 960 with the whole width free (checkers, 28 October 2026). */
@media (max-width: 63rem) {
    header.topbar:has(.topbar-place) .user-id {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        clip-path: inset(50%);
        white-space: nowrap;
    }

    header.topbar:has(.topbar-place) .user-chip {
        gap: 6px;
        padding-right: 6px;
    }
}

@media (max-width: 48rem) {
    /* The name leaves the screen, not the accessibility tree. */
    .user-id {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .user-chip {
        gap: 6px;
        padding-right: 6px;
    }

    /* Home as its icon alone; the word stays for screen readers. */
    .topbar-home {
        width: 34px;
        padding: 0;
    }

    .topbar-home > span {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .topbar-home > svg {
        margin: 0;
    }
}

@media (max-width: 40rem) {
    .shell > header.topbar { gap: var(--step); }
    .topbar-tools { gap: 6px; }

    .user-menu {
        margin-left: 0;
        padding-left: 0;
        border-left: 0;
    }

    .theme button {
        padding: 0 6px;
        font-size: 0.75rem;
    }

    .mark img { height: 30px; }
    .user-chip > .icon { display: none; }
    .user-chip { padding: 3px; }
}

/* Under 360 it did wrap: at 320 the mark (64) and the buttons (248) with
 * the gap between them came to 320 pixels of a 288-pixel row, so the
 * buttons took a line of their own under the mark and the header was three
 * rows again (review, 22 October 2026). Every button is kept, at its own
 * height; only the air goes - the gaps between the buttons and beside the
 * mark, two pixels off each side of Home and the clear-screen button, three
 * off each side of a theme word and one round the badge - and the mark a
 * little smaller again. 279 pixels at 320: one row, with room over. */
@media (max-width: 22.5rem) {
    .mark img { height: 26px; }
    .shell > header.topbar { column-gap: 6px; }
    .topbar-tools { gap: 4px; }

    .topbar-tools .topbar-home,
    .topbar-tools .topbar-calm { width: 30px; }

    .theme button { padding: 0 3px; }
    .user-chip { padding: 2px; }
}

/* A thumb is not a cursor. */
@media (pointer: coarse) {
    .action,
    .action.ghost,
    .bar .action,
    .head-right .action { min-height: 44px; }

    .theme button { min-height: 32px; }
    .user-chip,
    .user-item { min-height: 44px; }

    /* A word-sized control - Retire, Amend, Change, Mark paid, the x that
     * takes a passenger off - was the height of its letters, 13 to 20
     * pixels (review, 28 October 2026). It keeps its look and its place in
     * the line and is a thumb's height to press: the padding reaches out
     * above and below its letters to 44 pixels and the margin takes the
     * room back, as the Flights list's "cancel" does on a phone
     * (office.css) - there only to 34, and its own rule outweighs this one:
     * its card's lines are 5 pixels apart, and 44 would reach over the date
     * above it and the invoice line under it (checkers, 28 October 2026).
     * Not out to the sides, where the word beside it is; a lone x is made 44
     * wide instead.
     *
     * Not in a popup: the shared account's Change PIN, Rename and Retire
     * sit in a row that wraps on a narrow phone, and a 44-pixel box on the
     * second line reached up over the first, so a tap on Change PIN could
     * open Retire. Popups have their own look (review, 28 October 2026). */
    .linkish:not(:where(dialog, .modal) *) {
        min-width: 44px;
        padding-block: calc((44px - 1em) / 2);
        margin-block: calc((1em - 44px) / 2);
    }
}

/* ---------------------------------------- optional, same pass: tile icons
 *
 * The lavender gradient square behind each section icon is the single most
 * template-looking thing on the front page. The same chip as a secondary
 * button; it turns primary when the tile is pointed at, and stops wobbling. */

.module-mark {
    border: 1px solid var(--btn-border);
    border-radius: var(--btn-radius);
    background: var(--btn-bg);
    box-shadow: var(--btn-shadow);
    color: var(--text);
}

.module:hover .module-mark,
.module:focus-visible .module-mark {
    background: var(--btn-primary-bg);
    border-color: var(--btn-primary-border);
    color: var(--btn-primary-text);
    transform: none;
}

.module[aria-disabled='true'] .module-mark {
    background: var(--btn-ghost-bg);
    box-shadow: none;
    color: var(--text-dim);
}

/* ------------------------------------------------- where you are, up top
 *
 * "Operations / Flights" beside the mark, divided from it by a hairline.
 * It used to be a card in the middle of the page - two words given a whole
 * band of screen (Hussein, 3 October 2026). The section is a step back up;
 * the screen itself is the heavier of the two. */

.topbar-tools { margin-left: auto; }

.topbar-place {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
    margin-left: 4px;
    padding-left: 18px;
    border-left: 1px solid var(--border);
    font-family: var(--font-ui);
    line-height: 1.2;
    white-space: nowrap;
}

.topbar-place .place-up {
    color: var(--text-dim);
    font-size: 0.9375rem;
    font-weight: 500;
    text-decoration: none;
    border-radius: 4px;
}

.topbar-place a.place-up:hover { color: var(--text); }

.topbar-place .place-sep {
    color: var(--text-faint);
    font-size: 1rem;
    font-weight: 300;
}

.topbar-place .place-here {
    overflow: hidden;
    text-overflow: ellipsis;
    color: var(--text);
    font-size: 1.0625rem;
    font-weight: 650;
    letter-spacing: -0.01em;
}

/* A module with no buttons left in its bar no longer has the bar; one with
 * buttons keeps them, on the right where they always were. */
.bar.tools-only { justify-content: flex-end; }

/* The buttons keep their own size whatever the name beside them; it is the
 * name that gives way, with an ellipsis rather than running under them.
 * Between 641 and 700 pixels a long one - Administration / Documentation,
 * Settings / Fuel Suppliers - squeezed the buttons instead, the badge ended
 * 51 pixels past the window's edge and the whole screen slid sideways
 * (found by the Admin Block and Sub Charter chats, 28 October 2026). */
.topbar-tools { flex-shrink: 0; }

.topbar-place .place-up {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Where the two do not fit, the section gives way first and the screen's
 * own name last: they gave way together, as "Adminis... / Documen...",
 * when the section alone could take the whole cut (checkers, 28 October
 * 2026). At 100 to the name's 1 the name still took a fifth of a pixel of
 * a 6-pixel cut, and any cut at all is an ellipsis - "Operati... / Cancelled
 * Briefin..." on the shared Ops account at 1009; this leaves it nothing to
 * see until the section has gone. */
.topbar-place .place-up { flex-shrink: 100000; }

/* A tablet and a narrow window: on the line under the mark, as on a phone,
 * where the whole of it fits - beside the mark there was room for about 120
 * pixels of it at 660, and up to about 855, where Home has its word, not
 * quite all of Administration / Documentation. */
@media (max-width: 56rem) {
    .shell > header.topbar {
        flex-wrap: wrap;
        row-gap: var(--step);
    }

    .topbar-place {
        order: 3;
        flex-basis: 100%;
        margin: 2px 0 0;
        padding-left: 0;
        border-left: 0;
    }
}

/* The same up to 1200 while Install app is in the header - Chrome or Edge
 * before the portal is installed, Hussein's own, or an iPad. The 56rem
 * above was measured without it, and its 120 pixels cut the name beside
 * the mark from 897 to about 1180: "Engin... / Consignmen..." on a 1024
 * laptop (checkers, 28 October 2026). */
@media (max-width: 75rem) {
    .shell > header.topbar:has(.topbar-install:not([hidden])) {
        flex-wrap: wrap;
        row-gap: var(--step);
    }

    .shell > header.topbar:has(.topbar-install:not([hidden])) .topbar-place {
        order: 3;
        flex-basis: 100%;
        margin: 2px 0 0;
        padding-left: 0;
        border-left: 0;
    }
}

/* A phone: the screen's own name only - the header row is already the mark,
 * Home, the theme and the person. */
@media (max-width: 40rem) {
    .topbar-place .place-up,
    .topbar-place .place-sep { display: none; }
}

/* ------------------------------------------- the primary, on the dark theme
 *
 * It was paper-white: a hole in the page rather than a button on it. Now the
 * Tanzanair blue, lifted enough to read against the night-navy background,
 * with white on it - the same button as daylight, a shade brighter. */
:root,
:root[data-theme='dark'] {
    --btn-primary-bg: #3651d4;
    --btn-primary-bg-hover: #4260e6;
    --btn-primary-bg-active: #2d45b8;
    --btn-primary-border: #2a40ad;
    --btn-primary-text: #ffffff;
    --btn-primary-shadow: inset 0 1px 0 rgb(255 255 255 / 0.18), 0 1px 0 rgb(0 0 0 / 0.5), 0 2px 8px -2px rgb(54 81 212 / 0.45);
}

/* ------------------------------------------------------- dialogs, finished
 *
 * Every add-and-edit dialog in the portal: a raised panel with a real edge
 * and shadow, a header that states the record, fields that look like fields,
 * and labels that read as words rather than as stencilled codes. */

:root,
:root[data-theme='dark'] {
    --dialog-bg: #0f1436;
    --dialog-band: rgb(255 255 255 / 0.025);
    --dialog-edge: rgb(255 255 255 / 0.10);
    --dialog-shadow: 0 0 0 1px rgb(0 0 0 / 0.4), 0 30px 80px -20px rgb(0 0 0 / 0.75);

    --field-bg: #0a0e29;
    --field-border: rgb(255 255 255 / 0.12);
    --field-border-hover: rgb(255 255 255 / 0.22);
    --field-shadow: inset 0 1px 2px rgb(0 0 0 / 0.25);
    --field-ring: rgb(157 176 255 / 0.28);
    --field-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a3a9c9' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");

    --doc-bg: rgb(255 255 255 / 0.035);
    --doc-border: rgb(255 255 255 / 0.10);
    --doc-tile: #1b2257;
    --doc-tile-ink: #c5ccf5;
    --doc-ok: #3fcf8e;
    --doc-due: #f0b232;
}

:root[data-theme='light'] {
    --dialog-bg: #ffffff;
    --dialog-band: #f6f7fb;
    --dialog-edge: rgb(19 23 53 / 0.10);
    --dialog-shadow: 0 0 0 1px rgb(19 23 53 / 0.06), 0 30px 70px -20px rgb(19 23 53 / 0.35);

    --field-bg: #ffffff;
    --field-border: #cdd2e1;
    --field-border-hover: #aab0c8;
    --field-shadow: 0 1px 1px rgb(19 23 53 / 0.04);
    --field-ring: rgb(45 75 209 / 0.18);
    --field-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%234a5178' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");

    --doc-bg: #ffffff;
    --doc-border: #dde0eb;
    --doc-tile: #eef0fa;
    --doc-tile-ink: #202b7d;
    --doc-ok: #1f9d63;
    --doc-due: #c27c0e;
}

@media (prefers-color-scheme: light) {
    :root:not([data-theme]) {
        --dialog-bg: #ffffff;
        --dialog-band: #f6f7fb;
        --dialog-edge: rgb(19 23 53 / 0.10);
        --dialog-shadow: 0 0 0 1px rgb(19 23 53 / 0.06), 0 30px 70px -20px rgb(19 23 53 / 0.35);

        --field-bg: #ffffff;
        --field-border: #cdd2e1;
        --field-border-hover: #aab0c8;
        --field-shadow: 0 1px 1px rgb(19 23 53 / 0.04);
        --field-ring: rgb(45 75 209 / 0.18);
        --field-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%234a5178' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");

        --doc-bg: #ffffff;
        --doc-border: #dde0eb;
        --doc-tile: #eef0fa;
        --doc-tile-ink: #202b7d;
        --doc-ok: #1f9d63;
        --doc-due: #c27c0e;
    }
}

.modal {
    border: 1px solid var(--dialog-edge);
    border-radius: 14px;
    background: var(--dialog-bg);
    box-shadow: var(--dialog-shadow);
}

.modal .sheet-head {
    padding: 18px 24px;
    border-bottom: 1px solid var(--dialog-edge);
    background: var(--dialog-bg);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

.modal .sheet-head h1 {
    font-family: var(--font-ui);
    font-size: 1.1875rem;
    font-weight: 650;
    letter-spacing: -0.015em;
    line-height: 1.25;
}

.sheet-title { min-width: 0; }

.sheet-sub {
    margin: 3px 0 0;
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    font-variant-numeric: tabular-nums;
}

.modal .head-right { gap: 8px; }

/* ---- fields ---- */

.modal .inputs label > span:not(.reg) {
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    font-weight: 550;
    letter-spacing: 0;
    text-transform: none;
}

.modal .inputs label { gap: 6px; }

/* Fields that look like fields: every dialog, and the phone page's form
 * (Submit a Briefing), which sets its own sizes and colours on top in
 * submit.css. */
:is(.modal, .phone-form) input:not([type='checkbox']):not([type='radio']):not([type='file']):not([type='hidden']),
:is(.modal, .phone-form) select,
:is(.modal, .phone-form) textarea {
    min-height: 40px;
    padding: 0 12px;
    border: 1px solid var(--field-border);
    border-radius: 8px;
    background-color: var(--field-bg);
    box-shadow: var(--field-shadow);
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 0.9375rem;
    transition: border-color 120ms ease, box-shadow 120ms ease;
}

:is(.modal, .phone-form) textarea { padding-top: 9px; padding-bottom: 9px; }

/* The figures keep the data face: a serial, a docket number. */
.modal input.ap { font-family: var(--font-data); font-size: 0.9375rem; }

:is(.modal, .phone-form) select {
    appearance: none;
    -webkit-appearance: none;
    padding-right: 34px;
    background-image: var(--field-arrow);
    background-repeat: no-repeat;
    background-position: right 12px center;
    background-size: 12px 12px;
    text-overflow: ellipsis;
    cursor: pointer;
}

:is(.modal, .phone-form) input:not([type='checkbox']):not([type='radio']):hover,
:is(.modal, .phone-form) select:hover,
:is(.modal, .phone-form) textarea:hover { border-color: var(--field-border-hover); }

:is(.modal, .phone-form) input:not([type='checkbox']):not([type='radio']):focus,
:is(.modal, .phone-form) select:focus,
:is(.modal, .phone-form) textarea:focus {
    outline: none;
    border-color: var(--focus);
    border-radius: 8px;
    box-shadow: 0 0 0 3px var(--field-ring);
}

/* ---- the flight dialog ---- */

/* Four columns, each row one thought: which flight; who paid; who flew it
 * and on what fuel. The billing row is whole, so hiding it for an FOC
 * flight leaves no gap. */
#flight-dialog .flight-fields {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 18px 14px;
    align-items: start;
    padding: 22px 24px 26px;
}

#flight-dialog .flight-fields .span-half { grid-column: span 2; }
#flight-dialog .flight-fields .span-all { grid-column: 1 / -1; }
#flight-dialog .flight-fields .field-row { display: contents; }
#flight-dialog .flight-fields .field-row[hidden] { display: none; }

#flight-dialog input[type='date'] {
    font-family: var(--font-data);
    font-size: 0.875rem;
    font-variant-numeric: tabular-nums;
}

@media (max-width: 52rem) {
    #flight-dialog .flight-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 30rem) {
    #flight-dialog .flight-fields {
        grid-template-columns: minmax(0, 1fr);
        padding: 18px 16px 22px;
    }

    #flight-dialog .flight-fields .span-half { grid-column: 1 / -1; }
}

/* ---- the paperwork ----
 *
 * A band across the top of the dialog, a shade apart from the fields, with a
 * card per document. Filed, missing and not-needed each have their own face:
 * a file tile and a green dot; a dashed outline that is itself the Attach
 * button; and a faded card that is plainly not a gap. */

.doc-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
    padding: 18px 24px;
    border-bottom: 1px solid var(--dialog-edge);
    background: var(--dialog-band);
}

.doc-grid .doc {
    display: flex;
    align-items: center;
    gap: 12px;
    min-width: 0;
    min-height: 62px;
    padding: 10px 10px 10px 12px;
    border: 1px solid var(--doc-border);
    border-radius: 10px;
    background: var(--doc-bg);
    font-family: var(--font-ui);
}

.doc-icon {
    position: relative;
    flex: none;
    display: grid;
    place-items: center;
    width: 36px;
    height: 40px;
    border-radius: 7px;
    background: var(--doc-tile);
    color: var(--doc-tile-ink);
}

/* The file-type tag on the corner of the tile, as a desktop draws it. */
.doc-icon > b {
    position: absolute;
    left: -5px;
    bottom: 5px;
    padding: 1px 4px;
    border-radius: 3px;
    background: #d93b3b;
    color: #ffffff;
    font-family: var(--font-ui);
    font-size: 0.5625rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    line-height: 1.3;
}

.doc-text {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
}

.doc-name {
    overflow: hidden;
    color: var(--text);
    font-size: 0.875rem;
    font-weight: 600;
    line-height: 1.25;
    text-decoration: none;
    text-overflow: ellipsis;
    white-space: nowrap;
}

a.doc-name:hover { text-decoration: underline; text-underline-offset: 2px; }

.doc-state {
    display: flex;
    align-items: center;
    gap: 6px;
    overflow: hidden;
    color: var(--text-faint);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.is-filed .doc-state::before,
.is-missing .doc-state::before {
    content: '';
    flex: none;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--doc-ok);
}

.is-missing .doc-state::before { background: var(--doc-due); }

.doc-do {
    display: flex;
    align-items: center;
    gap: 2px;
    margin-left: auto;
    flex: none;
}

.doc-btn {
    display: inline-grid;
    place-items: center;
    width: 32px;
    height: 32px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 7px;
    background: transparent;
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    cursor: pointer;
    transition: background-color 120ms ease, color 120ms ease;
}

.doc-btn:hover {
    background: var(--btn-ghost-bg-hover);
    color: var(--text);
}

.doc-btn.danger:hover {
    background: var(--btn-danger-wash-hover);
    color: var(--btn-danger-ink);
}

.doc-btn:focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: 1px;
}

.doc-btn > svg { width: 16px; height: 16px; }

/* The file input lies invisibly over its button (or, for a missing document,
 * the whole card) rather than display:none, so it can still be reached with
 * the Tab key - and the ring is drawn on what it covers. */
.doc-grid .doc,
.doc-btn { position: relative; }

.doc-grid .doc input[type='file'] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    min-height: 0;
    margin: 0;
    padding: 0;
    opacity: 0;
    cursor: pointer;
}

.doc-btn:has(input:focus-visible),
.doc-grid .doc.is-missing:has(> input:focus-visible) {
    outline: 2px solid var(--focus);
    outline-offset: 1px;
}

/* The no-script Send, a word rather than an icon. Hidden the moment the
 * script runs, and the [hidden] has to be said: display above beats it. */
.doc-btn.send-pick { width: auto; padding: 0 10px; }
.doc-btn[hidden] { display: none; }
.doc-grid > .send-pick { grid-column: 1 / -1; justify-self: start; }

/* Missing: the whole card attaches. */
.doc-grid .doc.is-missing {
    border-style: dashed;
    background: transparent;
    cursor: pointer;
    transition: border-color 120ms ease, background-color 120ms ease;
}

.doc-grid .doc.is-missing .doc-icon {
    background: transparent;
    outline: 1px dashed var(--doc-border);
    outline-offset: -1px;
    color: var(--text-faint);
}

.doc-attach {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 30px;
    padding: 0 11px;
    border: 1px solid var(--btn-border);
    border-radius: 6px;
    background: var(--btn-bg);
    box-shadow: var(--btn-shadow);
    color: var(--btn-text);
    font-size: 0.8125rem;
    font-weight: 550;
    transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
}

.doc-attach > svg { width: 14px; height: 14px; opacity: 0.8; }

.doc-grid .doc.is-missing:hover { border-color: var(--focus); background: var(--doc-bg); }

.doc-grid .doc.is-missing:hover .doc-attach {
    border-color: var(--btn-primary-border);
    background: var(--btn-primary-bg);
    color: var(--btn-primary-text);
}

/* Not needed on this kind of flight - present, so the three always line up,
 * but plainly not a gap in the paperwork. */
.doc-grid .doc.is-none {
    border-style: dashed;
    background: transparent;
    opacity: 0.5;
}

.doc-grid .doc.is-none .doc-icon { background: transparent; color: var(--text-faint); }

/* A further fuel note whose number is known and whose paper is still to
 * come: dashed like a gap, with the amber dot, but not a button - it is
 * attached from somewhere else. */
.doc-grid .doc.is-awaited {
    border-style: dashed;
    background: transparent;
}

.doc-grid .doc.is-awaited .doc-icon { background: transparent; color: var(--text-faint); }

.is-awaited .doc-state::before {
    content: '';
    flex: none;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--doc-due);
}

/* The briefing as sent and, under it in the same cell, the costed briefing
 * Finance attaches beside it (19 October 2026). The cards beside a pair keep
 * their own height rather than stretching to the pair's. */
.doc-grid:has(> .doc-stack) { align-items: start; }

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

/* "Attach costed briefing": the papers' Attach button on its own, not a
 * dashed card - a dashed card is a paper the flight is missing, and no
 * flight is short of a costed briefing. The file input lies over it, as it
 * does over the papers' own buttons. */
.doc-costed-add {
    position: relative;
    align-self: flex-start;
    display: inline-flex;
    margin-left: 2px;
    border-radius: 6px;
    cursor: pointer;
}

.doc-costed-add input[type='file'] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    min-height: 0;
    margin: 0;
    padding: 0;
    opacity: 0;
    cursor: pointer;
}

.doc-costed-add:hover .doc-attach {
    border-color: var(--btn-primary-border);
    background: var(--btn-primary-bg);
    color: var(--btn-primary-text);
}

.doc-costed-add:has(input:focus-visible) {
    outline: 2px solid var(--focus);
    outline-offset: 1px;
}

.doc-stack > .send-pick { align-self: flex-start; }

/* An invoice's papers in Finance: the same cards, sitting in the invoice's
 * sheet rather than in a dialog's band. */
.doc-grid.invoice-papers {
    padding: 0 calc(var(--step) * 3) calc(var(--step) * 2);
    border-bottom: 0;
    background: transparent;
}

.invoice-papers .paper-for {
    margin: 0 0 2px;
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.8125rem;
}

.invoice-papers .paper-for strong {
    color: var(--text);
    font-family: var(--font-data);
    font-variant-numeric: tabular-nums;
}

@media (max-width: 40rem) {
    .doc-grid {
        grid-template-columns: minmax(0, 1fr);
        padding: 14px 16px;
    }

    .modal .sheet-head { padding: 14px 16px; }

    /* The whole screen on a phone, so no corners and no edge. */
    .modal {
        border: 0;
        border-radius: 0;
        box-shadow: none;
    }
}

@media (pointer: coarse) {
    .doc-btn { width: 40px; height: 40px; }
}

@media (prefers-reduced-motion: reduce) {
    .doc-grid .doc,
    .doc-btn,
    .doc-attach { transition: none; }
}

/* A toolbar whose title went up to the header keeps its count - "42 active",
 * "312 airfields" - and that line is now the only words in the bar, so it
 * reads as the status of the list rather than as a footnote to a heading. */
.who > .data:only-child {
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.875rem;
    font-weight: 500;
}

/* A dialog block with no heading over it (Hussein, 3 October 2026): the
 * fields start a comfortable step below the edge rather than flush with it. */
.modal .block > .inputs:first-child { padding-top: 20px; }

.modal .sheet-flash { margin: 16px 24px 0; }

/* A tick box at the height of the fields beside it, in the button blue,
 * and not painted over by the field rules meant for text boxes. */
.modal .inputs .tick { min-height: 40px; }

.modal .inputs .tick input[type='checkbox'] {
    appearance: auto;
    -webkit-appearance: checkbox;
    flex: none;
    width: 18px;
    height: 18px;
    min-height: 0;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    box-shadow: none;
    accent-color: var(--btn-primary-bg);
    cursor: pointer;
}

.modal .inputs label.tick > span:not(.reg) {
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 0.875rem;
    font-weight: 500;
}

/* The browser's own controls - tick boxes, the date picker, scroll bars -
 * follow the theme chosen in the header, not the computer's. With only
 * "light dark" declared, a laptop in dark mode drew a black tick box on the
 * light theme's white dialog. */
:root[data-theme='light'] { color-scheme: light; }
:root[data-theme='dark'] { color-scheme: dark; }

/* ------------------------------------------------- popups, the calm look
 *
 * Hussein, 27 October 2026, of the Sub charter popup and Add a sub-charter
 * supplier: "very ugly UI". Every popup built on the quotation sheet - a
 * .modal holding a .sheet, its groups in .block, its fields in .inputs -
 * still wore the sheet's working dress: group headings in ten-pixel grey
 * typewriter capitals spaced out across the line, field names in the dim
 * colour, hints in the faint one, and fields packed by quote.css into as
 * many columns as would fit - so a Name, a Phone and an Email came out three
 * widths, a name with "optional" after it wrapped onto two lines and dropped
 * its box below its neighbours', and Notes sat alone on a row of its own.
 *
 * Now they read as the Bookings popups do (bookings.css, "the popups"),
 * which Hussein had already passed:
 *
 *   - a field's name 14px, in the text colour, above its box; "optional" a
 *     small dim word on the same line, never a line of its own;
 *   - a hint under a box 13px in the dim colour, which reads at more than
 *     4.5:1 on both themes - the faint one is not for anything to be read;
 *   - a group headed in words, 15px and semibold, in sentence case;
 *   - two even columns on a PC and one on a phone, a long field (span2) the
 *     whole width, every box in a row level with the others;
 *   - 16px between fields, 24px between groups and at the edges;
 *   - on a phone the whole screen, the main button pinned along the foot of
 *     it where a thumb is.
 *
 * Every rule here leaves out .bk-dialog: the Bookings popups have their own,
 * finished, and are not to move. A popup with a layout of its own - the
 * flight's four columns, the person's form on Users, the customer's cards -
 * keeps it, by its own more particular rules further down. */

.modal:not(.bk-dialog) { --pop-edge: 24px; }

/* The page behind darkened, on every popup. One the server draws open and
 * no script has reopened as a modal - with no script, or while the passcode
 * is asked over it (popups.js waits for that) - is not in the top layer and
 * so has no ::backdrop: it sat over the list in full colour, as if it were
 * part of the page. A shadow wide enough to cover the window is the same
 * dark veil, and leaves the page, and the passcode box over it, as they
 * were. */
.modal:not(.bk-dialog)[open]:not(:modal) {
    box-shadow: var(--dialog-shadow), 0 0 0 100vmax rgb(4 6 20 / 0.62);
}

.modal:not(.bk-dialog) .sheet-sub { margin-top: 4px; font-size: 0.875rem; }

.modal:not(.bk-dialog) .block { border-bottom-color: var(--dialog-edge); }

/* A group's heading: words, not a stencil. */
.modal:not(.bk-dialog) .block-title {
    padding: 24px var(--pop-edge) 12px;
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 0.9375rem;
    font-weight: 600;
    letter-spacing: 0;
    line-height: 1.35;
    text-transform: none;
}

/* Two even columns; a long field, or a tick that heads a group, across
 * both. span3 and span4 are the same here - a field meant to be the width
 * of the form. */
.modal:not(.bk-dialog) .inputs {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 16px;
    align-items: start;
    padding: 0 var(--pop-edge) 24px;
}

.modal:not(.bk-dialog) .inputs > :is(.span2, .span3, .span4) { grid-column: 1 / -1; }

.modal:not(.bk-dialog) .block > .inputs:first-child { padding-top: 24px; }

/* A note said about the boxes above it sits under them, not a group away. */
.modal:not(.bk-dialog) .inputs:has(+ .foot-note) { padding-bottom: 12px; }

.modal:not(.bk-dialog) .inputs label {
    gap: 6px;
    min-width: 0;
}

/* A field's name: the first words in its label. A tick's words come after
 * its box, and a field with its mark inside it (the customer's) is not a
 * name - this rule takes neither. */
.modal:not(.bk-dialog) .inputs label:not(.tick) > span:first-child:not(.reg),
.modal:not(.bk-dialog) .inputs .cust-label {
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 0.875rem;
    font-weight: 550;
    letter-spacing: 0;
    line-height: 1.4;
    text-transform: none;
}

/* "optional", after the name: small, dim, and kept to the name's line. */
.modal:not(.bk-dialog) .inputs label > span > .reg {
    margin-left: 2px;
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    font-weight: 400;
    letter-spacing: 0;
    white-space: nowrap;
}

/* A hint under a box, and a note under a group. */
.modal:not(.bk-dialog) .inputs label > span.reg,
.modal:not(.bk-dialog) .reg {
    min-height: 0;
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    letter-spacing: 0;
    line-height: 1.45;
    text-transform: none;
}

.modal:not(.bk-dialog) .foot-note {
    padding: 0 var(--pop-edge) 16px;
    color: var(--text-dim);
    font-size: 0.8125rem;
    line-height: 1.5;
}

/* One inside the fields' own grid is already inside the edges. */
.modal:not(.bk-dialog) .inputs > .foot-note { padding: 0; }

/* A note at the foot of a popup (New role, System) inside its edges,
 * rather than running into them. */
.modal:not(.bk-dialog) .sheet > .note { margin: 0 var(--pop-edge) 24px; }

/* What a group tells rather than asks - a supplier's aircraft listed on its
 * Amend, "No pilots yet.", a note - inside the same edges as the fields
 * (found 27 October 2026: the list's bullets stood outside the popup's edge
 * and its notes ran from one side of it to the other, and past it on a
 * phone). */
.modal:not(.bk-dialog) .block > :is(.note, .sub-list, .sub-none) { margin: 0 var(--pop-edge) 12px; }
.modal:not(.bk-dialog) .block > :is(.note, .sub-list, .sub-none):last-child { margin-bottom: 24px; }
.modal:not(.bk-dialog) .block > .foot-note:last-child { padding-bottom: 24px; }

/* A note in a popup is a few words to read, so a block of them, as every
 * note already is on a phone (above, "A note is [accent bar][text]"): as
 * .note's flex row each link in a sentence was a piece of its own, with a
 * gap either side of it - "Change these on the Aircraft screen / Pilots
 * screen ." on the supplier's Amend. The bar becomes the left border. */
.modal:not(.bk-dialog) :is(.sheet, .block) > .note {
    display: block;
    border-left: 3px solid var(--caution);
}

.modal:not(.bk-dialog) :is(.sheet, .block) > .note.warn { border-left-color: var(--alarm); }

.modal:not(.bk-dialog) :is(.sheet, .block) > .note::before { display: none; }

/* A note's emphasis in the popup's own letters, and free to wrap - the
 * typewriter face is for serials and figures, not for a sentence. */
.modal:not(.bk-dialog) .note.prose strong {
    font-family: inherit;
    white-space: normal;
}

/* A table's column heads in a popup (What a role can reach, Add
 * passengers, Create Invoice's briefings) in words, as a field's name is,
 * not in spaced-out capitals. */
.modal:not(.bk-dialog) table thead th {
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: 0;
    text-transform: none;
}

/* A tick beside a field sits level with that field's box, below its name;
 * one the width of the form needs no such step. */
.modal:not(.bk-dialog) .inputs > label:not(.tick, .span2, .span3, .span4) + label.tick:not(.span2, .span3, .span4) { margin-top: calc(0.875rem * 1.4 + 6px); }

.modal:not(.bk-dialog) .inputs label.tick { gap: 10px; }

.modal:not(.bk-dialog) .inputs label.tick > span:not(.reg) { font-size: 0.9375rem; }

/* The other kind of tick (.check, Flight Types): its words in the text
 * colour, not the dim one, and its box in the button blue as .tick's is. */
.modal:not(.bk-dialog) .inputs label.check { gap: 10px; min-height: 40px; }

.modal:not(.bk-dialog) .inputs label.check > span {
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 0.9375rem;
    font-weight: 500;
}

.modal:not(.bk-dialog) .inputs label.check input[type='checkbox'] { accent-color: var(--btn-primary-bg); }

.modal:not(.bk-dialog) .sheet-flash { margin: 16px var(--pop-edge) 0; }

/* .action is inline-flex, which beats the hidden attribute. */
.modal:not(.bk-dialog) .action[hidden] { display: none; }

/* On a phone: one column, the edges a thumb's width, and the main button -
 * Save, Use this aircraft, Retire it - out of the title bar and pinned along
 * the foot of the screen, where it is reached without scrolling back up to
 * it. Cancel stays beside the title. Room is kept under the last field so
 * the button never sits over it. */
@media (max-width: 40rem) {
    .modal:not(.bk-dialog) { --pop-edge: 16px; }

    /* The whole screen even when no script has made it a modal (no script
     * at all, or the passcode asked over it): left where the page drew it,
     * New role sat 203 pixels down, under the header, the theme switch and
     * the tabs, and ran that far off the foot of the screen. */
    .modal:not(.bk-dialog, .sheet-modal)[open]:not(:modal) {
        position: fixed;
        inset: 0;
    }

    /* A table in a popup laid out as cards: each line's label in words, as
     * a field's name is (the portal's cards name them in spaced capitals). */
    .modal:not(.bk-dialog) :is(table.rates, table.compare) tbody td::before {
        color: var(--text-dim);
        font-family: var(--font-ui);
        font-size: 0.8125rem;
        font-weight: 550;
        letter-spacing: 0;
        text-transform: none;
    }

    .modal:not(.bk-dialog) .inputs { grid-template-columns: minmax(0, 1fr); }
    .modal:not(.bk-dialog) .inputs > * { grid-column: 1 / -1; }
    .modal:not(.bk-dialog) .inputs > label:not(.tick, .span2, .span3, .span4) + label.tick:not(.span2, .span3, .span4) { margin-top: 0; }

    .modal:not(.bk-dialog) .block-title { padding-top: 20px; }

    .modal:not(.bk-dialog) .sheet-head {
        flex-wrap: nowrap;
        align-items: flex-start;
        gap: 12px;
        padding: 12px var(--pop-edge);
    }

    .modal:not(.bk-dialog) .sheet-head .head-right {
        flex: none;
        width: auto;
    }

    .modal:not(.bk-dialog) .sheet-head .head-right > .action:is(.primary, .danger):last-child {
        position: fixed;
        z-index: 3;
        left: 16px;
        right: 16px;
        bottom: calc(12px + env(safe-area-inset-bottom, 0px));
        min-height: 48px;
        font-size: 1rem;
        justify-content: center;
        box-shadow: var(--dialog-shadow);
    }

    /* On the sheet rather than the dialog, so a popup that already makes
     * this room itself - the flight's and the cancel window (office.css) -
     * says how much, and it is not made twice. */
    .modal:not(.bk-dialog):has(> .sheet > .sheet-head .head-right > .action:is(.primary, .danger):last-child) > .sheet {
        padding-bottom: calc(76px + env(safe-area-inset-bottom, 0px));
    }
}

/* Create Invoice (Finance > Reservations), the one popup built on the
 * costing sheet rather than the quotation sheet, in the same look (27
 * October 2026): its title in the popups' letters, not the typewriter face;
 * "Tally Invoice No" a field's name above its box, not a ten-pixel stencil
 * beside it; what is ticked so far in words; everything inside the same
 * edges; and on a phone the whole screen - it stopped 12 pixels short of
 * every edge - with Save pinned along the foot and Close beside the title.
 * The briefings' table keeps its own columns. */
.modal.sheet-modal .costing-head {
    align-items: center;
    padding: 18px var(--pop-edge);
    border-bottom-color: var(--dialog-edge);
    background: var(--dialog-bg);
}

.modal.sheet-modal .costing-who { display: block; min-width: 0; }

.modal.sheet-modal .costing-who h1 {
    font-family: var(--font-ui);
    font-size: 1.1875rem;
    font-weight: 650;
    letter-spacing: -0.015em;
    line-height: 1.25;
}

.modal.sheet-modal .costing-who p { margin-top: 4px; font-size: 0.875rem; }

.modal.sheet-modal .costing-acts { gap: 8px; }

.modal.sheet-modal .costing-figures {
    padding: 24px var(--pop-edge);
    border-bottom-color: var(--dialog-edge);
}

.modal.sheet-modal .costing-figures label {
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
}

.modal.sheet-modal .costing-figures label > span {
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 0.875rem;
    font-weight: 550;
    letter-spacing: 0;
    line-height: 1.4;
    text-transform: none;
    white-space: normal;
}

.modal.sheet-modal .invoice-fields label.wide input { width: 16rem; max-width: 100%; }

.modal.sheet-modal .pick-bar {
    gap: 12px 16px;
    padding: 16px var(--pop-edge);
    border-bottom-color: var(--dialog-edge);
}

.modal.sheet-modal .pick-billed { gap: 8px; font-size: 0.875rem; color: var(--text); }

/* Its ticks as every popup's are: the browser's own, 18 pixels, in the
 * button blue - not quote.css's field rules, which drew each briefing's as a
 * filled dark square the width of its column. */
.modal.sheet-modal :is(.pick-billed, table.invoice-pick) input[type='checkbox'] {
    appearance: auto;
    -webkit-appearance: checkbox;
    flex: none;
    width: 18px;
    height: 18px;
    min-height: 0;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    box-shadow: none;
    accent-color: var(--btn-primary-bg);
    vertical-align: middle;
    cursor: pointer;
}

.modal.sheet-modal .pick-summary { font-family: var(--font-ui); font-size: 0.8125rem; }

.modal.sheet-modal table.costing-lines thead th { border-bottom-color: var(--dialog-edge); }

.modal.sheet-modal table.costing-lines .l { padding-left: var(--pop-edge); }

/* The ticks inside the edge too: they sat 9 pixels from the popup's side.
 * And the customer, last, stops short of the far one. */
.modal.sheet-modal table.costing-lines .v { padding-left: var(--pop-edge); padding-right: 0; }
.modal.sheet-modal table.costing-lines :is(th, td):last-child { padding-right: var(--pop-edge); }

/* A briefing a row a finger can land on, now its tick no longer props the
 * row open: 40 pixels, as a popup's field is. */
.modal.sheet-modal table.invoice-pick tbody td { padding-top: 10px; padding-bottom: 10px; }

.modal.sheet-modal .costing-note { padding: 12px var(--pop-edge); }

@media (max-width: 40rem) {
    .modal.sheet-modal {
        top: 0;
        left: 0;
        transform: none;
        width: 100vw;
        max-width: 100vw;
        height: 100dvh;
        max-height: 100dvh;
        border: 0;
        border-radius: 0;
    }

    .modal.sheet-modal .costing-head {
        flex-wrap: nowrap;
        align-items: flex-start;
        gap: 12px;
        padding: 12px var(--pop-edge);
    }

    .modal.sheet-modal .costing-acts > .action.primary:last-child {
        position: fixed;
        z-index: 3;
        left: 16px;
        right: 16px;
        bottom: calc(12px + env(safe-area-inset-bottom, 0px));
        min-height: 48px;
        font-size: 1rem;
        justify-content: center;
        box-shadow: var(--dialog-shadow);
    }

    .modal.sheet-modal .sheet.costing { padding-bottom: calc(76px + env(safe-area-inset-bottom, 0px)); }

    .modal.sheet-modal .costing-figures label,
    .modal.sheet-modal .invoice-fields label.wide input { width: 100%; }

    /* One scroll, the popup's, rather than a list scrolling in a box inside
     * a screen that scrolls. */
    .modal.sheet-modal .pick-scroll { max-height: none; overflow: visible; }

    /* A date kept to one line ("15 Aug 2026", not three), the columns
     * closer, so the customer has the room that is left. */
    .modal.sheet-modal table.costing-lines .l { padding-left: 12px; }
    .modal.sheet-modal table.costing-lines td.data { white-space: nowrap; }
    .modal.sheet-modal table.costing-lines .v { width: auto; }
}

/* The flight list's customer column: a name, so it reads from its first
 * letter rather than centred, and a long one ("G4S Security Services
 * Tanzania Ltd") is cut short with its full name on hover rather than
 * folding the row onto two lines. */
table.flights:not(.access) th.name,
table.flights:not(.access) td.name { text-align: left; }

/* At most 20rem, and less on a narrower laptop: at 1024 pixels the full
 * 20rem made the flight list 47 pixels wider than the screen, and a list
 * wider than the screen scrolls in a box of its own (portal.js) where one
 * that fits holds its headings under the pinned row (22 October 2026). */
table.flights td.name .who-flew {
    display: block;
    max-width: min(20rem, 26vw);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ----------------------------------------------- search, in the bar
 *
 * The search box lives in the bar beside Back, not in a card of its own
 * under it (Hussein, 3 October 2026), and answers as you type. A magnifier
 * drawn in the field rather than a Search button beside it: there is no
 * button to press any more. */

:root,
:root[data-theme='dark'] {
    --search-glass: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%237d84ab' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
}

:root[data-theme='light'] {
    --search-glass: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%237c83a6' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
}

@media (prefers-color-scheme: light) {
    :root:not([data-theme]) {
        --search-glass: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%237c83a6' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
    }
}

.bar-search {
    flex: 1 1 16rem;
    max-width: 30rem;
    margin: 0 0 0 auto;
}

/* Beside a count that keeps its note on its own line ("3 suppliers
 * matching "dev" · all") the search starts smaller, and still grows into
 * what is left: at 16rem, Sub Charter Suppliers' Add and Back went onto a
 * second line at 641-735 as the note appeared, the held bar grew past what
 * may be held and let go, and the list jumped 52 pixels (checkers, 28
 * October 2026). */
.bar:has(> .who.inline) > .bar-search { flex-basis: 10rem; }

.bar-search input[type='search'] {
    width: 100%;
    min-height: var(--control-h);
    padding: 0 12px 0 36px;
    border: 1px solid var(--field-border);
    border-radius: 8px;
    background: var(--field-bg) var(--search-glass) no-repeat 12px center / 16px 16px;
    box-shadow: var(--field-shadow);
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 0.9375rem;
    transition: border-color 120ms ease, box-shadow 120ms ease;
}

.bar-search input[type='search']::placeholder { color: var(--text-faint); }
.bar-search input[type='search']:hover { border-color: var(--field-border-hover); }

.bar-search input[type='search']:focus {
    outline: none;
    border-color: var(--focus);
    box-shadow: 0 0 0 3px var(--field-ring);
}

/* While the answer is on its way: the glass fades, nothing jumps. */
.bar-search.busy input[type='search'] { background-blend-mode: luminosity; }

.bar-do {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--step);
}

/* The count, and the way to see all of them, on one line. */
.who.inline {
    flex-direction: row;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 4px 12px;
}

.who .bar-link {
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    font-weight: 500;
    text-decoration: underline;
    text-decoration-color: var(--border);
    text-underline-offset: 3px;
}

.who .bar-link:hover { color: var(--text); text-decoration-color: currentColor; }

/* A filter in use: the Filters button says so with a dot, so a short list
 * is never a mystery. */
.action.is-set::after {
    content: '';
    width: 7px;
    height: 7px;
    margin-left: 2px;
    border-radius: 50%;
    background: var(--focus);
}

/* The panel opens from the button, so its own heading row is not needed. */
#flight-filters > summary { display: none; }
#flight-filters:not([open]) { display: none; }
#flight-filters .filter-form label > span {
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    font-weight: 550;
    letter-spacing: 0;
    text-transform: none;
}

@media (max-width: 40rem) {
    .bar-search {
        order: 3;
        flex-basis: 100%;
        max-width: none;
        margin: 0;
    }

    /* A finger's height wherever the box stands - in a bar, in a list's
     * own row of tools - not only in the row under a tab strip (.list-tools
     * above): Invoicing's, Passengers', the issued quotations', the
     * consignment lists' were 36 pixels beside 44-pixel buttons (review,
     * 28 October 2026). */
    .bar-search input[type='search'] { min-height: 44px; }
}

/* And on a touch tablet, beside buttons that are 44 there too: at 700 the
 * Consignments search was 36 tall beside its 44-pixel Show links
 * (checkers, 28 October 2026). */
@media (pointer: coarse) {
    .bar-search input[type='search'] { min-height: 44px; }
}

/* ---------------------------------------------- missing briefings
 *
 * The reason once, at the top, beside the button that uses it; a tick on
 * each serial it applies to. */

.settle-bar {
    display: flex;
    align-items: flex-end;
    flex-wrap: wrap;
    gap: 12px;
    margin-bottom: calc(var(--step) * 2);
    padding: 14px 16px;
    border: var(--edge);
    border-radius: var(--radius);
    background: var(--surface);
}

.settle-why {
    display: flex;
    flex: 1 1 22rem;
    flex-direction: column;
    gap: 6px;
}

.settle-why > span {
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    font-weight: 550;
}

.settle-why input {
    min-height: var(--control-h);
    padding: 0 12px;
    border: 1px solid var(--field-border);
    border-radius: 8px;
    background: var(--field-bg);
    box-shadow: var(--field-shadow);
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 0.9375rem;
}

.settle-why input:focus {
    outline: none;
    border-color: var(--focus);
    box-shadow: 0 0 0 3px var(--field-ring);
}

.settle-bar .action[disabled] {
    opacity: 0.45;
    cursor: not-allowed;
    box-shadow: none;
}

table.gap-list .tick-col { width: 3rem; }

table.gap-list input[type='checkbox'] {
    width: 18px;
    height: 18px;
    min-height: 0;
    margin: 0;
    padding: 0;
    accent-color: var(--btn-primary-bg);
    cursor: pointer;
    vertical-align: middle;
}

table.gap-list tbody tr.ticked td { background: rgb(54 81 212 / 0.08); }

table.gap-list .action.ghost {
    min-height: 30px;
    padding: 0 10px;
    font-size: 0.8125rem;
}

/* A list screen starts under the header. Centred in the window, a short
 * list floated with a band of empty page above it - the space the header
 * was cleared to save. The front door and the module tiles stay centred. */
.middle.wide { justify-content: flex-start; }

/* ------------------------------------------------ clear the screen
 *
 * The radar button beside Home. Pressed, every panel folds into it (the
 * movement is in portal.js) and the page is left to the map and the
 * aeroplane crossing it. The scope's sweep turns while the screen is clear,
 * which is also the sign of where to press to bring it all back. */

.topbar-calm {
    flex: none;
    width: 34px;
    min-height: 34px;
    padding: 0;
}

.topbar-calm > svg {
    width: 18px;
    height: 18px;
    margin: 0;
}

.topbar-calm .sweep {
    transform-box: view-box;
    transform-origin: 12px 12px;
}

/* Cleared: the pieces are hidden rather than removed, so whatever was half
 * typed is still there when they come back. */
html.calm .shell > main > *,
html.calm .topbar-place,
html.calm .topbar-tools > :not([data-calm]) {
    visibility: hidden;
}

/* The header's rule goes with its contents; the page does not scroll under
 * a screen with nothing on it. */
.shell > header.topbar { transition: border-color 500ms ease; }

html.calm .shell > header.topbar,
html.calm-going .shell > header.topbar { border-bottom-color: transparent; }

html.calm,
html.calm body { overflow: hidden; }

/* The button, alone on a clear screen: lit, its sweep turning, and a slow
 * ring going out from it - a scope that is on, and the one thing to press. */
html.calm .topbar-calm {
    border-color: var(--btn-primary-border);
    background: var(--btn-primary-bg);
    color: var(--btn-primary-text);
    box-shadow: var(--btn-primary-shadow);
}

html.calm .topbar-calm::after {
    content: '';
    position: absolute;
    inset: -1px;
    border-radius: inherit;
    pointer-events: none;
    box-shadow: 0 0 0 0 var(--btn-primary-bg);
}

@media (prefers-reduced-motion: no-preference) {
    html.calm .topbar-calm .sweep {
        animation: radar-sweep 2.6s linear infinite;
    }

    html.calm .topbar-calm::after {
        animation: radar-ring 2.6s ease-out infinite;
    }
}

@keyframes radar-sweep {
    to { transform: rotate(360deg); }
}

@keyframes radar-ring {
    0% { box-shadow: 0 0 0 0 rgb(54 81 212 / 0.45); }
    70% { box-shadow: 0 0 0 14px rgb(54 81 212 / 0); }
    100% { box-shadow: 0 0 0 14px rgb(54 81 212 / 0); }
}

/* The map is the whole screen now, so it can be a little more present. */
.field { transition: filter 900ms ease; }
html.calm .field { filter: saturate(1.25) brightness(1.12); }

@media (max-width: 48rem) {
    .topbar-calm { width: 34px; }
}

/* ------------------------------------------------ roles: how far each reaches
 *
 * A slim gauge and the figure beside it: how many of the portal's open
 * screens the role may view. A role nobody has set up yet says so in amber
 * instead of showing an empty bar. */
.reach {
    display: inline-flex;
    align-items: center;
    gap: 12px;
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    color: var(--text-dim);
    font-variant-numeric: tabular-nums;
}

.reach-bar {
    position: relative;
    flex: none;
    width: 112px;
    height: 6px;
    overflow: hidden;
    border-radius: 999px;
    background: var(--seg-track);
    box-shadow: inset 0 0 0 1px var(--seg-track-border);
}

.reach-bar > span {
    position: absolute;
    inset: 0 auto 0 0;
    border-radius: inherit;
    background: var(--btn-primary-bg);
}

.reach.none { color: var(--doc-due); }
.reach.none .reach-bar { opacity: 0.6; }

/* Edit access: a quiet text-and-icon control that reads as a link to the
 * role's grid, not a grey slab on every row. It firms up when the row is
 * pointed at, and sits above the row's own link so it is pressable. */
.role-do { width: 1%; white-space: nowrap; }

.role-access {
    position: relative;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    min-height: 32px;
    padding: 0 10px 0 11px;
    border: 1px solid transparent;
    border-radius: var(--btn-radius);
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    font-weight: 550;
    text-decoration: none;
    transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
}

.role-access > svg { flex: none; opacity: 0.75; }
.role-access > svg:last-child { margin-left: -2px; opacity: 0.5; transition: transform 140ms ease; }

.rates tbody tr.go:hover .role-access,
.role-access:focus-visible {
    border-color: var(--btn-border);
    background: var(--btn-bg);
    box-shadow: var(--btn-shadow);
    color: var(--btn-text);
}

.rates tbody tr.go:hover .role-access > svg:last-child { transform: translateX(2px); opacity: 0.8; }

.role-access:hover {
    border-color: var(--btn-primary-border);
    background: var(--btn-primary-bg);
    color: var(--btn-primary-text);
}

.role-access:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

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

/* How much of the list is showing, under the list. Quiet: it is a status,
 * and its link is the only thing in it to press. */
.list-foot {
    margin: 10px 2px 0;
    color: var(--text-faint);
    font-family: var(--font-ui);
    font-size: 0.8125rem;
}

.list-foot a {
    margin-left: 8px;
    color: var(--text-dim);
    font-weight: 550;
    text-decoration: underline;
    text-decoration-color: var(--border);
    text-underline-offset: 3px;
}

.list-foot a:hover { color: var(--text); text-decoration-color: currentColor; }

/* Under a finger the link is a thumb's size to press, as a word-sized
 * .linkish is ("a thumb is not a cursor"): Airports' Clear after a search
 * was 30 by 17, beside 44-pixel buttons, and the "all" after Sub Charter
 * Suppliers' "matching ..." the same (checkers, 28 October 2026). The
 * padding reaches out round the word and the margin takes the room back,
 * so the line does not move. */
@media (pointer: coarse) {
    .list-foot a,
    .who .hint a {
        display: inline-block;
        min-width: 44px;
        padding-block: calc((44px - 1em) / 2);
        margin-block: calc((1em - 44px) / 2);
        line-height: 1;

        /* The word where it was, the extra width after it: centred, "all"
         * stood 15 pixels off the dot before it (checkers, 28 October 2026). */
        text-align: start;
    }
}

/* ------------------------------------------------ the briefing checklist
 *
 * A row per briefing and a mark per field: a green tick for filed, a red
 * cross for missing, a faint dash for a field that kind of flight never
 * carries. The marks are small round chips so a column of them reads as a
 * column, and a row of ticks reads as done at a glance. The fields are
 * grouped - flight, fuel, scans, billing - with a hairline between groups. */

:root,
:root[data-theme='dark'] {
    --ck-yes: #3fcf8e;
    --ck-yes-wash: rgb(63 207 142 / 0.14);
    --ck-no: #ff6b5f;
    --ck-no-wash: rgb(255 90 78 / 0.15);
}

:root[data-theme='light'] {
    --ck-yes: #17885a;
    --ck-yes-wash: rgb(23 136 90 / 0.11);
    --ck-no: #c5281c;
    --ck-no-wash: rgb(197 40 28 / 0.09);
}

@media (prefers-color-scheme: light) {
    :root:not([data-theme]) {
        --ck-yes: #17885a;
        --ck-yes-wash: rgb(23 136 90 / 0.11);
        --ck-no: #c5281c;
        --ck-no-wash: rgb(197 40 28 / 0.09);
    }
}

table.checklist th,
table.checklist td { text-align: center; }

table.checklist th.name,
table.checklist td.name { text-align: left; }

table.checklist .ck-groups th {
    padding-bottom: 4px;
    border-bottom: 0;
}

table.checklist th.ck-group {
    position: relative;
    color: var(--text-dim);
}

/* A rule under each group's name, spanning its columns, so the four groups
 * read as four. */
table.checklist th.ck-group::after {
    content: '';
    position: absolute;
    left: 10px;
    right: 10px;
    bottom: 0;
    height: 1px;
    background: var(--border);
}

/* As narrow at the sides as the marks under them (.ck below). With the
 * table's usual padding the headings, not the marks, set each column's
 * width, and sixteen of them pushed the briefing number and its date onto
 * two lines on a 1366 laptop (22 October 2026). */
table.checklist th.ck-head {
    padding-left: 6px;
    padding-right: 6px;
    font-size: 0.625rem;
    white-space: nowrap;
}

table.checklist .ck-first { border-left: 1px solid var(--border); }

table.checklist td.name strong { font-family: var(--font-data); }

.ck-when {
    margin-left: 8px;
    color: var(--text-faint);
    font-family: var(--font-data);
    font-size: 0.75rem;
}

.ck { padding-left: 6px; padding-right: 6px; }

.ck-mark {
    display: inline-grid;
    place-items: center;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    vertical-align: middle;
}

.ck-mark > svg { stroke-width: 2.6; }

.ck-yes .ck-mark { background: var(--ck-yes-wash); color: var(--ck-yes); }
.ck-no .ck-mark { background: var(--ck-no-wash); color: var(--ck-no); }
.ck-na .ck-mark { color: var(--text-faint); opacity: 0.45; }

.ck-score {
    color: var(--ck-no);
    font-family: var(--font-data);
    font-size: 0.8125rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.ck-score > span { color: var(--text-faint); font-weight: 400; }
.ck-score.full { color: var(--ck-yes); }

/* How many more fuel notes a flight has beyond its first, beside the mark -
 * a round trip refuelled at both ends has two. */
.ck-more {
    margin-left: 3px;
    color: var(--text-dim);
    font-family: var(--font-data);
    font-size: 0.6875rem;
    font-weight: 600;
    vertical-align: middle;
}

/* Optional and not filled: an empty ring, neither a tick nor a cross. */
.ck-opt .ck-mark {
    width: 12px;
    height: 12px;
    border: 1.5px solid var(--text-faint);
    opacity: 0.5;
}

/* A tab keeps its words on one line. */
.tab { white-space: nowrap; }

/* The month heading reads from the left, as it does on the flight list. */
table.checklist tr.month-break th { text-align: left; }

/* ------------------------------------------- headings that stay put
 *
 * On a long list the column headings stay put as the rows scroll under
 * them (Hussein, 3 October 2026) - now on every list in the portal and
 * directly under the pinned row, not at the top of the window behind it
 * (Hussein, 22 October 2026: "when scrolling the headings should be
 * frozen").
 *
 * The first go did it for a dozen tables marked sticky-head, and only on
 * screens 1024 pixels and wider. Every other table's headings scrolled
 * away, for two reasons that have nothing to do with the heading: the box
 * a table sits in, .table-scroll, scrolls sideways, and a sticky heading
 * can only stick to the nearest box that scrolls - one that never scrolls
 * up and down, so it never stuck; and table.rates was overflow: hidden,
 * which made the table itself such a box too.
 *
 * So portal.js marks every list in the middle of a screen "held" and looks
 * at the box it is in:
 *   - .fits, the table fits: the box stops being a scroll box at all, and
 *     the headings stick to the window, at --pin-h;
 *   - .is-wide, the table is wider than the box (the checklist on a 1024
 *     laptop, most lists on a tablet): the box keeps its sideways scroll,
 *     its own headings scroll with the rows, and a copy of them, .held-head,
 *     is held under the pinned row instead and slid sideways with the box
 *     (portal.js, "a wide list's headings"). Nothing is cut off, the page
 *     itself never slides sideways, and the page is the only thing that
 *     scrolls up and down - no second scrollbar beside the window's, and no
 *     box a finger gets stuck in.
 * Without script neither is set, the box scrolls sideways as it always
 * has, and the headings simply scroll with the rows.
 *
 * Not on a phone, where a list is cards and its headings are not drawn; nor
 * in a dialog or on a sheet, which pin their own heads; nor on paper, where
 * a printed table repeats its headings on every page by itself. */

:root,
:root[data-theme='dark'] { --head-solid: #151a37; }

:root[data-theme='light'] { --head-solid: #fafbfd; }

@media (prefers-color-scheme: light) {
    :root:not([data-theme]) { --head-solid: #fafbfd; }
}

/* Solid lists on a laptop too. The lists were see-through, --surface over
 * the page, so the moving map and its arc ran through every row at 1024 and
 * 1366 - the flights, the checklist, the papers, the missing serials,
 * Reservations, Users - while the held headings above them and the phone's
 * lists (office.css) were already solid. Hussein called it ugly (review,
 * 22 October 2026). Now every list in the middle of a screen is drawn on the
 * colour of its own held headings, so heading and rows are one solid panel
 * and the map stops at its edge. The colour is the see-through one as it
 * looked over the page, so nothing else about a list changes: its frame,
 * its rules, a row lit under the pointer, a month band.
 *
 * Only a list standing on the page. A list in a dialog, on a sheet (an
 * invoice's lines) or in a card sits on that panel's own colour and is left
 * on it; the Bookings screens draw their own, solid in their own card
 * colour (bookings.css - their headings are held like everybody else's
 * since 23 October 2026); a phone draws its lists as cards (below) or, on
 * the Flights screen, as office.css's panel. The customers' and employees'
 * tables stand in a frame of their own (drawn by quote.css), and the
 * airports' queries are panels of rows (.qgroup): those frames are made
 * solid instead, at every width, and lose a blur that had nothing left to
 * blur. The customers' frame is not named here by its class: this sheet
 * goes to the Ops phone, comments and all, and that name holds a word the
 * phone's files must never say (PhoneSeesNothingBehindTheScenesTest; review,
 * 26 October 2026). */
@media screen and (min-width: 40.0625rem) {
    main :is(table.rates, table.compare):not(dialog *, .modal *, .sheet *, .card *, .bk-page *) {
        background: var(--head-solid);
    }
}

@media screen {
    main :is(.compare-wrap, .qgroup):not(dialog *, .modal *, .bk-page *) {
        background: var(--head-solid);
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }
}

/* And solid cards on a phone. Below 40rem a list is a card a row (the 40rem
 * rules near the top), and each card was --surface, see-through, so the
 * moving map ran through Airports, Pilots, Passengers, Users, Customers,
 * Aircraft, an invoice's lines and every other list drawn as cards, while
 * the Flights screen's panel (office.css) and a laptop's lists were solid
 * (review, 22 October 2026). One rule for every card list, not one a
 * screen: each card is drawn on the same --head-solid, the see-through
 * colour as it looked over the page, so a card looks as it did with the
 * map stopped at its edge. A card lit under a finger, or holding a field
 * being typed in, is lit over the solid colour rather than instead of it.
 *
 * Cards on a sheet or in a card are solid too - the invoice's lines: those
 * panels are see-through on a phone, the map behind them. Not a list in a
 * dialog, whose panel is solid already; not the Flights screen's lists
 * (compact-rows), one solid panel of rows of their own; not the access grid
 * or a table kept a grid on a phone, which are not cards; and not the
 * Bookings screens, which draw their own. */
@media screen and (max-width: 40rem) {
    main :is(table.rates, table.compare):not(.access, .stays-table, .compact-rows, dialog *, .modal *, .bk-page *) > tbody > tr {
        background: var(--head-solid);
    }

    main :is(table.rates, table.compare):not(.access, .stays-table, .compact-rows, dialog *, .modal *, .bk-page *) > tbody > tr.go:is(:hover, :focus-within) {
        background: linear-gradient(var(--surface-hover), var(--surface-hover)) var(--head-solid);
    }
}

@media screen and (min-width: 40.0625rem) {
    .table-scroll.fits { overflow: visible; }

    /* clip, not visible: this box draws the rounded frame, and clip trims
     * the headings' square corners to it without making it a scroll box. */
    .compare-wrap.fits { overflow: clip; }

    /* Two pixels under the pinned row rather than flush with it. A held
     * heading leaves behind the table's own top border, which is drawn by
     * the table where the table starts and does not move with it, so the
     * top line of a held heading was a hole the tops of the rows' letters
     * showed through, under the pinned row (22 October 2026). Tucked under
     * the row, the hole is covered. */
    table.held > thead th {
        position: sticky;
        top: calc(var(--pin-h) - 2px);
        z-index: 4;
        background: var(--head-solid);
        box-shadow: inset 0 -1px 0 var(--border);
    }

    /* The checklist has two heading rows: the groups, then the fields under
     * them. The first is held to a fixed height so the second knows where
     * to stop. */
    table.held.checklist .ck-groups th { height: 34px; box-shadow: none; }
    table.held.checklist > thead > tr:nth-child(2) > th { top: calc(var(--pin-h) + 32px); }
    table.held.checklist th[rowspan] { box-shadow: inset 0 -1px 0 var(--border); }

    /* In a box that scrolls sideways a sticky heading sticks to the box,
     * which never scrolls up and down - so it would only be pushed down
     * inside it, over the first rows. There the headings stay where they
     * are, and the copy below does the holding. */
    .is-wide > table.held > thead th,
    .is-wide > table.held.checklist > thead > tr > th { top: auto; }

    /* The copy (portal.js, "a wide list's headings"): a strip that takes
     * no room in the page, level with the real headings, held under the
     * pinned row by the window. flow-root, so the margins portal.js gives
     * it are its own and never run together with the ones around it. Its
     * one child is the width of the inside of the box and cuts the copied
     * table off where the box does, slid sideways as far as the box is.
     * Hidden until the real headings near the pinned row, and
     * square-cornered while held, like the pinned bar. */
    .held-head {
        display: flow-root;
        position: sticky;
        top: calc(var(--pin-h) - 2px);
        z-index: 5;
        align-self: stretch;
        height: 0;
        visibility: hidden;
    }

    .held-head.shown { visibility: visible; }

    .held-head > div { overflow: hidden; }

    /* The copy is the top of a table, not a table: square along its foot,
     * where the rows run on under it. */
    .held-head table {
        margin: 0;
        border-bottom-left-radius: 0;
        border-bottom-right-radius: 0;
    }

    .held-head thead th {
        background: var(--head-solid);
        box-shadow: inset 0 -1px 0 var(--border);
    }

    .held-head table.checklist .ck-groups th { box-shadow: none; }
    .held-head table.checklist th[rowspan] { box-shadow: inset 0 -1px 0 var(--border); }

    .held-head.stuck table { border-top-left-radius: 0; border-top-right-radius: 0; }
}

/* A phone draws a list as cards, and paper repeats the headings by itself. */
@media (max-width: 40rem), print {
    .held-head { display: none; }
}

/* The checklist's month rows, styled as the flight list's are - a band of
 * their own, not a line that reads as part of the heading above it. */
table.checklist tbody tr.month-break th {
    padding: 12px 13px 6px;
    background: var(--bg);
    border-bottom: 1px solid var(--border);
    color: var(--text-faint);
    font-family: var(--font-display);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

/* The group names carry position: relative for the rule drawn under them,
 * which would otherwise beat the sticky above and let them scroll away. */
@media screen and (min-width: 40.0625rem) {
    table.held.checklist th.ck-group { position: sticky; }
}

/* ------------------------------------------------------------- pilots
 *
 * The period the figures cover, as a row of choices in the bar; the totals
 * under it; and on a pilot's own page, five figures across the top. */

.period {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 2px;
    padding: 2px;
    border: 1px solid var(--seg-track-border);
    border-radius: 8px;
    background: var(--seg-track);
    font-family: var(--font-ui);
}

.period > a {
    padding: 6px 12px;
    border-radius: 6px;
    color: var(--text-dim);
    font-size: 0.8125rem;
    font-weight: 550;
    text-decoration: none;
}

.period > a:hover { color: var(--text); }

.period > a.on {
    background: var(--seg-on);
    box-shadow: var(--seg-on-shadow);
    color: var(--text);
}

.period-custom {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-left: 4px;
}

.period-custom input[type='date'] {
    width: auto;
    min-height: 30px;
    padding: 0 6px;
    border: 1px solid var(--field-border);
    border-radius: 6px;
    background: var(--field-bg);
    color: var(--text);
    font-family: var(--font-data);
    font-size: 0.75rem;
}

.period-custom .action { min-height: 30px; padding: 0 10px; font-size: 0.8125rem; }

/* A thumb's worth on a touch screen. The chips were 32 pixels and the two
 * dates and Show 30 - the rule just above comes after "a thumb is not a
 * cursor" and beat its 44 for Show (review, 28 October 2026). One rule for
 * the Pilots screen and the Aircraft Report, which share the partial
 * (src/View/period_nav.php); Bookings draws its own period at its own sizes
 * (bookings.css). */
@media (pointer: coarse) {
    .period > a {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
    }

    .period-custom input[type='date'],
    .period-custom .action { min-height: 44px; }
}

.pilot-full {
    margin-left: 8px;
    color: var(--text-faint);
    font-size: 0.8125rem;
}

.pilot-part {
    display: block;
    color: var(--text-faint);
    font-family: var(--font-ui);
    font-size: 0.6875rem;
}

.pilot-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: 10px;
}

.pilot-stats > div {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 14px 16px;
    border: var(--edge);
    border-radius: var(--radius);
    background: var(--surface);
    font-family: var(--font-ui);
}

.pilot-stats span {
    color: var(--text-faint);
    font-size: 0.75rem;
    font-weight: 550;
}

.pilot-stats strong {
    color: var(--text);
    font-size: 1.375rem;
    font-weight: 650;
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.01em;
}

.pilot-stats em {
    color: var(--text-dim);
    font-size: 0.75rem;
    font-style: normal;
}

.pilot-stats > div.warn { border-color: var(--doc-due); }
.pilot-stats > div.warn em { color: var(--doc-due); }

.pilot-h {
    margin: 10px 2px -6px;
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.875rem;
    font-weight: 650;
}

/* A group's totals, under its rows: the column sums, set apart by a firmer
 * rule, and not a link - there is no one pilot or aircraft behind them. */
table.rates tbody tr.total td {
    border-top: 1px solid var(--arc);
    border-bottom: none;
    color: var(--text);
    font-weight: 600;
}

/* ------------------------------------------------------------ cross-check
 *
 * Under the Pilots and Aircraft Report tables: the flights, hours and miles
 * added up by aircraft, by captain and by co-pilot, side by side, with a
 * tick when the three agree and the briefings to look at when they do not.
 * Compact on purpose - it is a check on the figures above it, not a report
 * of its own. */

.cross-check {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-top: 6px;
    padding: 14px 16px;
    border: var(--edge);
    border-left: 3px solid var(--doc-ok);
    border-radius: var(--radius);
    background: var(--surface);
    font-family: var(--font-ui);
}

.cross-check.disagree { border-left-color: var(--alarm); }

.cc-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 10px;
    color: var(--text);
    font-size: 0.9375rem;
}

.cc-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: var(--doc-ok);
    color: #fff;
}

.cross-check.disagree .cc-mark { background: var(--alarm); }

.cc-sub {
    color: var(--text-faint);
    font-size: 0.8125rem;
}

.cc-totals {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 8px;
}

.cc-totals > div {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 8px 12px;
    border: 1px solid var(--border);
    border-radius: 8px;
}

.cc-totals span {
    color: var(--text-faint);
    font-size: 0.75rem;
    font-weight: 550;
}

.cc-totals strong {
    color: var(--text);
    font-size: 1rem;
    font-weight: 650;
    font-variant-numeric: tabular-nums;
}

.cc-totals em {
    color: var(--text-dim);
    font-size: 0.75rem;
    font-style: normal;
    font-variant-numeric: tabular-nums;
}

.cc-list summary {
    color: var(--doc-due);
    font-size: 0.8125rem;
    font-weight: 600;
    cursor: pointer;
}

.cc-list ul {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin: 8px 0 0;
    padding: 0;
    list-style: none;
    font-size: 0.8125rem;
    color: var(--text-dim);
}

.cc-list li {
    display: flex;
    flex-wrap: wrap;
    gap: 2px 10px;
}

.cc-list li a { color: var(--text); text-decoration: none; }
.cc-list li a:hover { text-decoration: underline; }

.cc-list li .data {
    min-width: 6.5rem;
    color: var(--text-faint);
    font-family: var(--font-data);
    font-size: 0.75rem;
}

@media (max-width: 40rem) {
    .period { width: 100%; }
    .cc-totals { grid-template-columns: minmax(0, 1fr); }
}

/* ------------------------------------------------------------- queries
 *
 * Raising one: the briefing, what it is about, what is needed, on one line.
 * And on the flight itself, the open query at the top of its dialog, in
 * amber - it is owed, not wrong. */

.query-raise .query-serial { flex: 0 0 9rem; }
.query-raise .query-kind { flex: 0 0 14rem; }

.settle-why select {
    min-height: var(--control-h);
    padding: 0 30px 0 12px;
    border: 1px solid var(--field-border);
    border-radius: 8px;
    background: var(--field-bg) var(--field-arrow) no-repeat right 10px center / 12px 12px;
    appearance: none;
    -webkit-appearance: none;
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 0.9375rem;
}

.query-close { display: inline; margin: 0; }
.query-close .role-access { cursor: pointer; font: inherit; font-size: 0.8125rem; font-weight: 550; }

.modal .query-flash {
    border-color: var(--doc-due);
    background: rgb(240 178 50 / 0.1);
}

@media (max-width: 40rem) {
    .query-raise .query-serial,
    .query-raise .query-kind { flex: 1 1 100%; }
}

/* ------------------------------------------------------------ passengers
 * Correcting a name (Hussein, 3 October 2026): a narrow one-field dialog,
 * and the button beside the person's name on their card.
 *
 * Narrow on a PC only. Said for every width, it beat the portal's own phone
 * rule, so on a phone the popup stopped 32px short of the right edge with
 * the page showing down that strip - and Save, pinned along the foot of the
 * screen since 27 October 2026, ran out past the popup onto it. */
@media (min-width: 40.0625rem) {
    #passenger-dialog { width: min(30rem, calc(100vw - 32px)); }
}

#passenger-dialog .inputs { grid-template-columns: minmax(0, 1fr); }
#passenger-dialog input[name='full_name'] { font-size: 1.0625rem; }

/* Add passengers: the manifest's lines as rows of boxes inside the popup's
 * edges, as its fields would be - not a framed table running into them -
 * and on a phone too, where they had been a card per line with the seat
 * and the infant tick labelled in capitals (27 October 2026). */
#passenger-dialog #manifest-lines {
    width: calc(100% - 2 * var(--pop-edge, 24px));
    min-width: 0;
    margin: 0 var(--pop-edge, 24px);
    table-layout: fixed;
    border: 0;
    border-radius: 0;
    background: none;
}

#passenger-dialog #manifest-lines :is(th, td) {
    padding: 0 12px 8px 0;
    border: 0;
    background: none;
    text-align: left;
    vertical-align: middle;
}

#passenger-dialog #manifest-lines th { padding-bottom: 6px; }
#passenger-dialog #manifest-lines :is(th, td):nth-child(2) { width: 5.5rem; }
#passenger-dialog #manifest-lines :is(th, td):nth-child(3) { width: 3.5rem; padding-right: 0; text-align: center; }
#passenger-dialog #manifest-lines tbody tr { background: none; }

/* The infant tick as every popup's tick is - the browser's own, 18 pixels,
 * in the button blue - not `.rates input`'s 36-pixel filled square, which
 * looked the same ticked or not. */
#passenger-dialog #manifest-lines input[type='checkbox'] {
    appearance: auto;
    -webkit-appearance: checkbox;
    width: 18px;
    height: 18px;
    min-height: 0;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    box-shadow: none;
    accent-color: var(--btn-primary-bg);
    vertical-align: middle;
    cursor: pointer;
}

#passenger-dialog .manifest-do { padding: 4px var(--pop-edge, 24px) 16px; }
#passenger-dialog .manifest-do:not(:has(> .action:not([hidden]))) { display: none; }

/* How a typed name is kept: a hint at the foot of the popup, in its own
 * letters, its first sentence in the text colour. */
#passenger-dialog .manifest-kept { margin: 0; padding-top: 8px; }
#passenger-dialog .manifest-kept strong { color: var(--text); font-weight: 600; }

.person-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--step);
    flex-wrap: wrap;
}

.person-head h3 { margin: 0; }

.flights .rowdo a.linkish {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    color: var(--text-dim);
    font-size: 0.8125rem;
    text-decoration: none;
}

.flights .rowdo a.linkish:hover { color: var(--text); }

/* ------------------------------------------------------- the customer form
 *
 * "This form is dull" (Hussein, 3 October 2026). It said nothing about who
 * it was for until you read the first box. Now the header does: a badge in
 * the colour of the kind of customer, the name as it will be invoiced, the
 * reference, and what they have flown with us - with the way through to
 * those flights. The fields sit on two soft cards, the kind is five choices
 * in plain sight, and the switches look like switches. */

.cust-dialog { --kind: #4f6bff; width: min(60rem, calc(100vw - 32px)); }
.cust-dialog[data-kind='individual'] { --kind: #12a594; }
.cust-dialog[data-kind='agent'] { --kind: #e38a0c; }
.cust-dialog[data-kind='ngo'] { --kind: #2e9e4f; }
.cust-dialog[data-kind='government'] { --kind: #9b5de5; }

:root[data-theme='dark'] .cust-dialog,
:root:not([data-theme]) .cust-dialog { --kind-wash: 22%; }
:root[data-theme='light'] .cust-dialog { --kind-wash: 13%; }

@media (prefers-color-scheme: light) {
    :root:not([data-theme]) .cust-dialog { --kind-wash: 13%; }
}

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

.cust-dialog .cust-head {
    position: relative;
    padding: 20px 24px;
    background:
        radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--kind) var(--kind-wash, 18%), transparent) 0%, transparent 55%),
        var(--dialog-bg);
    transition: background 240ms ease;
}

/* A rule of the kind's colour along the very top edge. */
.cust-dialog .cust-head::before {
    content: '';
    position: absolute;
    inset: 0 0 auto 0;
    height: 3px;
    background: var(--kind);
    transition: background 240ms ease;
}

.cust-id {
    display: flex;
    align-items: center;
    gap: 14px;
    min-width: 0;
}

.cust-mono {
    flex: none;
    display: grid;
    place-items: center;
    width: 48px;
    height: 48px;
    border-radius: 13px;
    background: var(--kind);
    box-shadow: 0 6px 18px -6px color-mix(in srgb, var(--kind) 70%, transparent), inset 0 1px 0 rgb(255 255 255 / 0.25);
    color: #fff;
    font-family: var(--font-ui);
    font-size: 1.0625rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    transition: background 240ms ease, box-shadow 240ms ease;
}

.cust-dialog .sheet-head h1 {
    overflow: hidden;
    max-width: 34rem;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 1.3125rem;
}

.cust-tags {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    margin: 6px 0 0;
}

.cust-ref,
.cust-kind,
.cust-new {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    height: 22px;
    padding: 0 8px;
    border-radius: 999px;
    font-family: var(--font-ui);
    font-size: 0.75rem;
    font-weight: 600;
    line-height: 1;
}

.cust-ref {
    border: 1px solid var(--dialog-edge);
    color: var(--text-dim);
    font-family: var(--font-data);
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
}

.cust-kind {
    background: color-mix(in srgb, var(--kind) 16%, transparent);
    color: color-mix(in srgb, var(--kind) 80%, var(--text));
}

.cust-kind[hidden],
.cust-new[hidden] { display: none; }

.cust-new {
    background: color-mix(in srgb, var(--doc-ok) 16%, transparent);
    color: var(--doc-ok);
}

/* ---- what they have flown with us ---- */

.cust-stats {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    border-bottom: 1px solid var(--dialog-edge);
    background: var(--dialog-band);
}

.cust-stats[hidden] { display: none; }

.cust-stat {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
    padding: 14px 24px;
    color: var(--text);
    text-decoration: none;
}

.cust-stat + .cust-stat { border-left: 1px solid var(--dialog-edge); }

.cust-stat .k {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--text-faint);
    font-family: var(--font-ui);
    font-size: 0.75rem;
    font-weight: 600;
}

.cust-stat strong {
    overflow: hidden;
    font-family: var(--font-ui);
    font-size: 1.0625rem;
    font-weight: 650;
    font-variant-numeric: tabular-nums;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The flights figure is the way to the flights. */
.cust-flights { position: relative; transition: background 140ms ease; }
.cust-flights strong { font-size: 1.375rem; line-height: 1.1; color: var(--kind); }

.cust-flights .go {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.75rem;
    font-weight: 600;
}

.cust-flights .go .icon { transition: transform 140ms ease; }
.cust-flights:hover { background: color-mix(in srgb, var(--kind) 8%, transparent); }
.cust-flights:hover .go { color: var(--text); }
.cust-flights:hover .go .icon { transform: translateX(3px); }

.cust-flights:focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: -2px;
}

/* ---- who they are ---- */

.cust-dialog .block { border-bottom: 0; }

/* Said with .cust-dialog in front, so the popups' shared two even columns
 * ("popups, the calm look", above) do not take this row's three to two. */
.cust-dialog .cust-who .inputs {
    grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
    padding-bottom: 14px;
}

.cust-dialog .cust-who .inputs > .span2 { grid-column: auto; }

.cust-who input[name='name'] { font-size: 1rem; font-weight: 600; }

.cust-kinds {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    padding: 0 24px 20px;
}

.cust-pick { position: relative; }

.cust-pick input {
    position: absolute;
    inset: 0;
    margin: 0;
    opacity: 0;
    cursor: pointer;
}

.cust-pick .pick {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    height: 38px;
    padding: 0 14px 0 12px;
    border: 1px solid var(--field-border);
    border-radius: 10px;
    background: var(--field-bg);
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.875rem;
    font-weight: 550;
    transition: border-color 140ms ease, background 140ms ease, color 140ms ease, box-shadow 140ms ease;
}

.cust-pick[data-kind='company'] { --pick: #4f6bff; }
.cust-pick[data-kind='individual'] { --pick: #12a594; }
.cust-pick[data-kind='agent'] { --pick: #e38a0c; }
.cust-pick[data-kind='ngo'] { --pick: #2e9e4f; }
.cust-pick[data-kind='government'] { --pick: #9b5de5; }

.cust-pick .pick .icon { color: var(--pick); }
.cust-pick:hover .pick { border-color: var(--field-border-hover); color: var(--text); }

.cust-pick input:checked + .pick {
    border-color: var(--pick);
    background: color-mix(in srgb, var(--pick) 14%, var(--field-bg));
    box-shadow: 0 0 0 1px var(--pick) inset;
    color: var(--text);
}

.cust-pick input:focus-visible + .pick { box-shadow: 0 0 0 3px var(--field-ring); }

/* ---- the two cards ---- */

.cust-pair {
    gap: 16px;
    padding: 0 24px 24px;
}

.cust-card {
    border: 1px solid var(--dialog-edge);
    border-radius: 12px;
    background: var(--dialog-band);
}

.cust-dialog .block.cust-card > .inputs {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 16px 14px;
    padding: 18px;
}

/* A field with its mark inside it. */
.cust-dialog .inputs label > span.fi {
    position: relative;
    display: block;
    color: var(--text-faint);
    font-size: inherit;
    font-weight: inherit;
}

.cust-dialog .fi .icon {
    position: absolute;
    top: 50%;
    left: 12px;
    transform: translateY(-50%);
    pointer-events: none;
}

.cust-dialog .fi input { width: 100%; padding-left: 36px !important; }
.cust-dialog .fi:focus-within .icon { color: var(--focus); }

/* Payment terms: the usual ones as chips, and the number they set. */
.cust-terms { display: flex; flex-direction: column; gap: 6px; }

.cust-label {
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    font-weight: 550;
}

.cust-terms-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
}

.cust-chips {
    display: inline-flex;
    padding: 3px;
    border: 1px solid var(--field-border);
    border-radius: 10px;
    background: var(--field-bg);
}

.cust-chip {
    height: 32px;
    padding: 0 11px;
    border: 0;
    border-radius: 7px;
    background: transparent;
    color: var(--text-dim);
    cursor: pointer;
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    transition: background 120ms ease, color 120ms ease;
}

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

.cust-chip.on {
    background: var(--btn-primary-bg);
    box-shadow: 0 1px 2px rgb(0 0 0 / 0.2);
    color: var(--btn-primary-text, #fff);
}

.cust-chip:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

.cust-days {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: var(--text-dim);
    font-family: var(--font-ui);
    font-size: 0.8125rem;
}

.cust-dialog .cust-days input { width: 4.75rem; text-align: center; font-variant-numeric: tabular-nums; }

/* VAT exempt as a switch. */
.cust-dialog .inputs label.cust-switch {
    flex-direction: row;
    align-items: center;
    gap: 12px;
    width: max-content;
    cursor: pointer;
}

.cust-switch input[type='checkbox'] {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
}

.cust-switch .track {
    position: relative;
    flex: none;
    width: 40px;
    height: 23px;
    border-radius: 999px;
    background: var(--field-border);
    transition: background 160ms ease;
}

.cust-switch .track::after {
    content: '';
    position: absolute;
    top: 3px;
    left: 3px;
    width: 17px;
    height: 17px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 3px rgb(0 0 0 / 0.3);
    transition: transform 160ms ease;
}

.cust-switch input:checked + .track { background: var(--doc-ok); }
.cust-switch input:checked + .track::after { transform: translateX(17px); }
.cust-switch input:focus-visible + .track { box-shadow: 0 0 0 3px var(--field-ring); }

.cust-dialog .inputs label.cust-switch > span.txt {
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 0.875rem;
    font-weight: 550;
}

.cust-later input:disabled {
    border-style: dashed !important;
    background: transparent !important;
    box-shadow: none !important;
    cursor: not-allowed;
}

/* ---- retiring, out of the way ---- */

.cust-retire {
    margin: 0 24px 22px;
    border: 1px solid var(--dialog-edge);
    border-radius: 12px;
}

.cust-retire summary {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 12px 16px;
    color: var(--text-faint);
    cursor: pointer;
    font-family: var(--font-ui);
    font-size: 0.8125rem;
    font-weight: 600;
    list-style: none;
}

.cust-retire summary::-webkit-details-marker { display: none; }
.cust-retire summary:hover { color: var(--alarm); }
.cust-retire[open] summary { color: var(--alarm); }
.cust-retire .retire { margin: 0; padding: 0 16px 16px; border-top: 0; }

/* Deleting sits under retiring, ruled off from it: the rarer of the two, and
 * the one that cannot be taken back from the screen. Where it is not allowed,
 * one line in its place says what holds the customer. */
.cust-retire .cust-delete { padding-top: 16px; border-top: 1px solid var(--dialog-edge); }
.cust-retire .cust-held { margin: 0; padding: 12px 16px 16px; border-top: 1px solid var(--dialog-edge); }

/* The forms are flex, which outranks the browser's own rule for hidden. */
.cust-retire[hidden],
.cust-retire .retire[hidden],
.cust-retire .cust-held[hidden] { display: none; }

@media (max-width: 52rem) {
    .cust-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .cust-stat:nth-child(3) { border-left: 0; }
    .cust-stat:nth-child(n + 3) { border-top: 1px solid var(--dialog-edge); }
    .cust-dialog .cust-who .inputs { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 40rem) {
    .cust-dialog { width: 100vw; }
    .cust-dialog .sheet-head h1 { max-width: 60vw; }
    .cust-stat { padding: 12px 16px; }
    .cust-pair { padding: 0 16px 16px; }
    .cust-kinds { padding: 0 16px 16px; }
    .cust-dialog .block.cust-card > .inputs { grid-template-columns: minmax(0, 1fr); }
    .cust-dialog .cust-card .inputs > * { grid-column: 1 / -1; }
}

@media (prefers-reduced-motion: reduce) {
    .cust-dialog .cust-head,
    .cust-mono,
    .cust-pick .pick,
    .cust-switch .track,
    .cust-switch .track::after,
    .cust-flights .go .icon { transition: none; }
}

/* The flights count on the customer list is a link to them, above the row's
 * own link to the form. */
.compare td.rowdo { position: relative; z-index: 2; }

.cust-flown {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 9px;
    border-radius: 999px;
    color: inherit;
    font-weight: 600;
    text-decoration: none;
    transition: background 120ms ease, color 120ms ease;
}

.cust-flown .icon { color: var(--text-faint); }
.cust-flown:hover { background: var(--dialog-band); color: var(--text); }
.cust-flown:hover .icon { color: currentColor; }

/* The kind chips are labels holding spans, and the field-label rules for
 * "label > span" reach them - small capitals in the data face. Undone. */
.cust-dialog .cust-pick > span.pick,
.cust-dialog .cust-pick > span.pick .txt {
    font-family: var(--font-ui);
    font-size: 0.875rem;
    font-weight: 550;
    letter-spacing: 0;
    text-transform: none;
}

.cust-dialog .cust-pick > span.pick { color: var(--text-dim); }
.cust-dialog .cust-pick:hover > span.pick,
.cust-dialog .cust-pick input:checked + span.pick { color: var(--text); }
.cust-dialog .cust-pick > span.pick { flex-direction: row; }

/* Always in sight on the passenger list - the hover-only reveal is what made
 * renaming look impossible. */
#passenger-results .rowdo .linkish { opacity: 1; }

/* The names a customer used to be on file under. */
.cust-aka {
    margin: 5px 0 0;
    color: var(--text-faint);
    font-family: var(--font-ui);
    font-size: 0.75rem;
}

.cust-aka span { color: var(--text-dim); }
.cust-aka[hidden] { display: none; }

/* The invoice's lines: money right-aligned, read from the last digit, with
 * the currency named in the headings and on the totals (Hussein, 5 October
 * 2026). The general rule above centres .num cells in these tables. */
.rates.invoice-lines th.num,
.rates.invoice-lines td.num,
.rates.invoice-list th.num,
.rates.invoice-list td.num { text-align: right; white-space: nowrap; }

/* ---------------------------------------------------------- sorting a list
 *
 * "keep options of sort ascending and descending in all screens where there
 * are lists, so we can sort as suited" (Hussein, 13 October 2026).
 *
 * table-sort.js puts the words of each heading on a list marked data-sort
 * inside a button. These rules make that button look like the heading it
 * was, with only the pointer, a lighter colour under it and a small arrow to
 * say it can be pressed. Quiet on purpose: a list is read for its rows, and
 * a row of headings that each looked like a button would shout over them.
 */
th > .sort-head {
    position: relative;
    display: inline-block;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 3px;
    background: none;
    color: inherit;
    font: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
    text-align: inherit;
    line-height: inherit;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
    transition: color 120ms ease;
}

th > .sort-head:hover,
th[aria-sort] > .sort-head { color: var(--text-dim); }

th[aria-sort] > .sort-head:hover { color: var(--text); }

th > .sort-head:focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: 3px;
    border-radius: 4px;
}

/* The arrow: two sides of a small square, turned to point up for lowest
 * first and down for highest first, in the heading's own colour.
 *
 * Drawn beside the words, out of the line, so the heading does not move when
 * it appears. A centred heading that shifted half an arrow's width on every
 * press would be the only thing on the screen anybody saw. The cell's own
 * padding is the room it sits in. Faint under the pointer, before anything
 * is pressed, as a hint of what pressing does. */
th > .sort-head::after {
    content: '';
    position: absolute;
    top: 50%;
    left: calc(100% + 0.5em);
    width: 0.45em;
    height: 0.45em;
    border-right: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: translateY(-15%) rotate(-135deg);
    opacity: 0;
    pointer-events: none;
    transition: opacity 120ms ease, transform 160ms ease;
}

th > .sort-head:hover::after,
th > .sort-head:focus-visible::after { opacity: 0.5; }

th[aria-sort='ascending'] > .sort-head::after,
th[aria-sort='descending'] > .sort-head::after { opacity: 1; }

th[aria-sort='descending'] > .sort-head::after { transform: translateY(-85%) rotate(45deg); }

/* On a telephone the headings are hidden and each row is a card (see the
 * 40rem rules near the top), so the same choices come as a list above the
 * cards instead. Only at that width; everywhere else the headings do it.
 * Not on a list with fewer than two rows to sort (table-sort.js hides it):
 * an empty list, or a list of one. */
.sort-pick { display: none; }

@media (max-width: 40rem) {
    .sort-pick {
        display: block;
        margin: 0 0 calc(var(--step) * 1.5);
    }

    .sort-pick[hidden] { display: none; }

    .sort-pick label {
        display: flex;
        align-items: center;
        gap: calc(var(--step) * 1.25);
        color: var(--text-dim);
        font-family: var(--font-ui);
        font-size: 0.875rem;
    }

    .sort-pick select {
        flex: 1 1 auto;
        min-width: 0;
        min-height: var(--control-h);
        padding: 0 30px 0 12px;
        border: 1px solid var(--field-border);
        border-radius: 8px;
        background: var(--field-bg) var(--field-arrow) no-repeat right 10px center / 12px 12px;
        box-shadow: var(--field-shadow);
        -webkit-appearance: none;
        appearance: none;
        color: var(--text);
        font-family: var(--font-ui);
        font-size: 0.9375rem;
    }

    .sort-pick select:focus-visible {
        outline: none;
        border-color: var(--focus);
        box-shadow: 0 0 0 3px var(--field-ring);
    }
}

/* Under a finger, a finger's height, as the search and the buttons over it
 * are: it was the one 36-pixel control above the cards on Documentation,
 * System, Pilots, Passengers and every other list (checkers, 28 October
 * 2026). And 16 pixels, or a phone zooms the page in when it is pressed
 * (as the Sort button's select below). */
@media (max-width: 40rem) and (pointer: coarse) {
    .sort-pick select {
        min-height: 44px;
        font-size: 1rem;
    }
}

/* The same choice as a button in the row of tools over a list
 * (table-sort.js, data-sort-pick): on the Flights screen's Flights, Queries,
 * Missing briefings, "Doesn't match its papers" and Cancelled tabs, beside
 * the search and Back, as the Checklist's row has everything on it - not a
 * line of its own, the width of the phone, between the search and the list
 * (review, 22 October 2026). A button the size of Back with the sort arrows
 * on it; the select lies over the whole of it unseen, so a press on the
 * button is a press on the select and the phone opens its own list of the
 * choices. Marked with the Filters button's dot while the list is in an
 * order somebody chose. Nowhere but a phone, never until the script has
 * put the choice in it, and not while it is hidden: a list with fewer than
 * two rows to sort. A finger square, 44 by 44, as Filters and Back are. */
.sort-slot { display: none; }

@media (max-width: 40rem) {
    .list-tools .sort-slot:has(.sort-pick:not([hidden])) {
        display: inline-flex;
        order: 1;
        width: 44px;
        min-height: 44px;
        padding: 0;
        overflow: hidden;
    }

    .list-tools .sort-slot > svg { margin: 0; }

    .sort-slot .sort-pick.in-slot,
    .sort-slot .sort-pick.in-slot label,
    .sort-slot .sort-pick.in-slot select {
        position: absolute;
        inset: 0;
        display: block;
        width: 100%;
        height: 100%;
        min-height: 0;
        margin: 0;
    }

    .sort-slot .sort-pick.in-slot label > span {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        clip-path: inset(50%);
        white-space: nowrap;
    }

    /* 16 pixels, or a phone zooms the page in when the select is pressed. */
    .sort-slot .sort-pick.in-slot select {
        padding: 0;
        border: 0;
        box-shadow: none;
        opacity: 0;
        font-size: 1rem;
        cursor: pointer;
    }

    .sort-slot:has(select:focus-visible) {
        outline: 2px solid var(--focus);
        outline-offset: 2px;
    }

    .sort-slot.is-set::after {
        position: absolute;
        top: 7px;
        right: 7px;
        margin: 0;
    }
}

/* Paper cannot be pressed. */
@media print {
    th > .sort-head::after { display: none; }
    .sort-pick { display: none !important; }
}

/* The change log (settings/audit.php) is read sixty entries at a time, so it
 * is sorted by the server and its headings are links to the list in another
 * order rather than buttons. Same look as every other sortable heading; a
 * link just brings an underline the button never had.
 *
 * On a telephone those headings are hidden like any other list's, and the
 * log's filter bar carries the same choices instead. Kept in the form at
 * every width, so that Show keeps the order, and seen only where it is the
 * only way to choose one. */
th > a.sort-head { text-decoration: none; }

label.order-pick { display: none; }

@media (max-width: 40rem) {
    label.order-pick { display: block; }
}

/* The cross-check's briefings to look at (src/View/cross_check.php), on the
 * Pilots screen and the Aircraft Report. A small table rather than a list,
 * so that it sorts like every other list. It was drawn quietly, as a table
 * of its own - no frame, no rules between lines - and so its headings were
 * never held under the pinned row, a phone never made its lines into cards,
 * and the serial's few letters were all there was to press. Since 28
 * October 2026 it is a list like any other (table.rates in a .table-scroll,
 * the row the link); what is its own is the reason in the quieter colour
 * and the date in the data face. The last column takes what width is left,
 * so the briefing and its date stay together on the left however long the
 * reason. */
.cc-list > .table-scroll { margin-top: 8px; }

.cc-table { color: var(--text-dim); }

.cc-table th { white-space: nowrap; }

.cc-table td:last-child { width: 100%; }

.cc-table td a { color: var(--text); text-decoration: none; }
.cc-table td a:hover { text-decoration: underline; }

.cc-table td.data {
    color: var(--text-faint);
    font-family: var(--font-data);
    font-size: 0.75rem;
    white-space: nowrap;
}

/* The telephone's Sort for a list the server puts in order
 * (src/View/list_order.php): the same look as the one table-sort.js draws,
 * with a button beside it to apply the choice, because there is no script
 * on these lists to apply it the moment it changes. */
@media (max-width: 40rem) {
    form.sort-pick {
        display: flex;
        align-items: center;
        gap: calc(var(--step) * 1.25);
    }

    form.sort-pick label { flex: 1 1 auto; min-width: 0; }
    form.sort-pick .action { flex: 0 0 auto; }
}

/* Briefings with no date, on the flight list and the checklist.
 *
 * They sit under their own heading above every month, because a page filed
 * without saying when it flew is the one needing attention (see the ORDER BY
 * in FlightRegister::all). Sorting must not undo that: a month heading moves
 * with a sort by date, and an empty date sorts to the bottom, where nobody
 * would look. So "No date" is drawn as a fixed row (data-sort-fixed) that
 * table-sort.js leaves where it is, with its own rows sorting under it - and
 * it cannot carry the month-break class, because that class is what tells
 * the sorter a heading moves. These rules give it the month heading's look. */
.flights tbody tr.undated-break th,
table.checklist tbody tr.undated-break th {
    text-align: left;
    background: var(--bg);
    border-bottom: 1px solid var(--border);
    color: var(--text-faint);
    font-family: var(--font-display);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.flights tbody tr.undated-break th { padding: calc(var(--step) * 0.75) calc(var(--step) * 1.6); }
table.checklist tbody tr.undated-break th { padding: 12px 13px 6px; }

.flights tbody tr.undated-break .count {
    display: inline-block;
    margin-left: calc(var(--step) * 0.75);
    padding: 0 calc(var(--step) * 0.9);
    border-radius: 999px;
    background: var(--surface-2, var(--surface));
    border: var(--edge);
    font-family: var(--font-data);
    font-size: 0.6875rem;
    letter-spacing: 0;
    color: var(--text-faint);
}

/* A search on a Flights tab that found nothing there, or found a number
 * that is not where it was looked for: one line, with its links. */
.search-miss { margin-bottom: calc(var(--step) * 1.5); }
.search-miss a { color: var(--text); font-weight: 550; text-underline-offset: 3px; }

/* ------------------------------------------- the portal as an app
 *
 * "Install app" (portal-install.js; Hussein, 19 October 2026). Both copies
 * stay hidden until the browser can install the portal; then the one that
 * fits is shown - in the header on a screen with room, in the person menu on
 * a phone, where the header row is already the mark, Home, the theme and the
 * person. [hidden] has to be said: display:inline-flex on .action beats it. */

.topbar-tools .topbar-install {
    flex: none;
    min-height: 34px;
    padding: 0 12px;
}

.topbar-tools .topbar-install[hidden],
.user-list .user-install { display: none; }

@media (max-width: 48rem) {
    .topbar-tools .topbar-install { display: none; }
    .user-list .user-install { display: block; }
    .user-list .user-install[hidden] { display: none; }
}

/* A menu entry that is a button, looking like the links beside it. */
button.user-item {
    width: 100%;
    border: 0;
    background: transparent;
    font-family: inherit;
    text-align: start;
    cursor: pointer;
}

/* The iPhone steps, and "Installed": a small card hung from the header's
 * right-hand end, over the page rather than pushing it down. */
.app-install-note {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    z-index: 60;
    width: 22rem;
    max-width: calc(100vw - 2rem);
    padding: 12px 14px;
    border: 1px solid var(--menu-border);
    border-radius: 8px;
    background: var(--menu-bg);
    box-shadow: var(--menu-shadow);
    font-family: var(--font-ui);
    font-size: 0.875rem;
    line-height: 1.5;
    color: var(--text);
}

.app-install-note[hidden] { display: none; }

.app-install-note p { margin: 0 0 8px; }

.app-install-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 4px;
}

html.calm .app-install-note { visibility: hidden; }

/* A thumb is not a cursor - as Home beside it. */
@media (pointer: coarse) {
    .topbar-tools .topbar-install { min-height: 44px; }
}

/* Ours, Sub-charter or All, on the Aircraft and Pilots screens (19 October
 * 2026). Three links side by side, the one showing marked; on a phone they
 * take a line of their own rather than squeezing the bar. */
.show-pick {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}

.show-pick .action { min-height: var(--control-h); }

/* Under a finger, a finger's height, as the period chips and the dates
 * beside them are: the rule above came after the touch screen's 44 for
 * every button and outweighed it, so Ours, Sub-charter and All were 36
 * (checkers, 28 October 2026). */
@media (pointer: coarse) {
    .show-pick .action { min-height: 44px; }
}

/* A sub-charter supplier's aircraft and pilots, read-only in its Amend dialog. */
.sub-list {
    margin: 0 0 8px;
    padding-left: 1.2rem;
    color: var(--text);
    font-size: 0.875rem;
    line-height: 1.7;
}

/* None yet, where the list would be: in the list's own size, dimmed - a
 * state, not a warning (27 October 2026; it was an amber note box). */
.sub-none {
    margin: 0 0 8px;
    color: var(--text-dim);
    font-size: 0.875rem;
    line-height: 1.7;
}

/* ------------------------------------------------------ who is working
 *
 * An account several people share - the Ops Office (Hussein, 20 October
 * 2026). "Who is working?" is a page of its own (src/View/working/
 * who_is_working.php) with a plain top bar, and the same question asked in
 * the page by working.js when something is about to be saved. The names
 * are chips, a thumb wide, as on the shared phone; the PIN box is the
 * passcode box. */

.working-top {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: calc(var(--step) * 1.5);
}

.working-account {
    flex: 1;
    min-width: 0;
    color: var(--text-dim);
    font-weight: 600;
    overflow-wrap: anywhere;
}

.working-card {
    width: min(34rem, 100%);
    margin: calc(var(--step) * 4) auto;
    padding: calc(var(--step) * 3);
    border: var(--edge);
    border-radius: var(--radius);
    background: var(--surface);
}

.working-card h1 {
    margin: 0 0 calc(var(--step) * 1.5);
    font-family: var(--font-display);
    font-size: 1.25rem;
    font-weight: 600;
    line-height: 1.3;
}

.working-lead,
.working-notice {
    margin: 0 0 calc(var(--step) * 1.5);
    color: var(--text-dim);
    line-height: 1.5;
}

.working-notice {
    padding: calc(var(--step) * 1.25) calc(var(--step) * 1.5);
    border-left: 3px solid var(--caution);
    color: var(--text);
}

.working-form {
    display: flex;
    flex-direction: column;
    gap: calc(var(--step) * 1.5);
}

.working-names {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin: 0;
    padding: 0;
    border: 0;
}

.working-name {
    position: relative;
    display: inline-flex;
    cursor: pointer;
}

/* The radio itself is out of sight; the chip beside it is what is pressed,
 * and what shows it is chosen. Still focusable, so the keyboard works. */
.working-name input {
    position: absolute;
    opacity: 0;
    inset: 0;
    margin: 0;
    cursor: pointer;
}

.working-name-text,
.working-dialog .working-chip {
    display: inline-flex;
    flex-direction: column;
    justify-content: center;
    min-height: 48px;
    padding: 8px 16px;
    border: 2px solid var(--border);
    border-radius: 12px;
    background: var(--bg);
    color: var(--text);
    font: 600 1rem var(--font-ui);
    line-height: 1.2;
    text-align: left;
    cursor: pointer;
    touch-action: manipulation;
}

.working-name-text small,
.working-dialog .working-chip small {
    color: var(--text-dim);
    font-size: 0.8125rem;
    font-weight: 500;
}

.working-name input:checked + .working-name-text,
.working-dialog .working-chip[aria-pressed='true'] {
    border-color: var(--violet);
    background: var(--violet);
    color: #ffffff;
}

.working-name input:checked + .working-name-text small,
.working-dialog .working-chip[aria-pressed='true'] small { color: #ffffff; }

.working-name input:focus-visible + .working-name-text {
    outline: 2px solid var(--focus);
    outline-offset: 2px;
}

.working-pin {
    display: grid;
    gap: 6px;
    color: var(--text-dim);
    font-size: 0.875rem;
}

/* The PIN box, on the page and in working.js's dialog: a text box with its
 * figures drawn as dots, never a password box. Everybody on the Ops PC
 * shares one browser, and a password box is one press of "Save password?"
 * from keeping somebody's PIN there for whoever sits down next. Chrome, Edge
 * and Safari draw the dots; Firefox since 114. */
.working-pin-box { -webkit-text-security: disc; }

.working-do {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--step);
}

/* The same question, asked over the page by working.js. */
.working-dialog { width: min(30rem, calc(100vw - var(--step) * 4)); }

.working-dialog .working-names { margin-top: calc(var(--step) * -0.5); }

/* "Janeth Mtesi is working now ..." - once, under the top bar. */
.working-said {
    margin: calc(var(--step) * 1.5) 0 0;
    padding: calc(var(--step) * 1.25) calc(var(--step) * 2);
    border: 1px solid var(--border);
    border-left: 3px solid var(--violet);
    border-radius: 8px;
    background: var(--surface);
    color: var(--text);
    font-size: 0.875rem;
    line-height: 1.45;
}

.working-said[hidden] { display: none; }

/* "Someone else is working", a form in the person menu. */
.user-hand-over { margin: 0; }

/* The people who use a shared account, in its person window on Users:
 * one line each, wrapping on a phone rather than scrolling. */
.shared-people {
    margin: calc(var(--step) * 2) 0 0;
    padding-top: calc(var(--step) * 1.5);
    border-top: 1px solid var(--border);
}

.shared-people-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 12px;
    margin: 0 0 var(--step);
}

.shared-people-head h2 {
    margin: 0;
    font-size: 0.95rem;
}

.shared-people-list {
    display: grid;
    gap: 4px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.shared-people-list li {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 14px;
    padding: 6px 0;
    border-bottom: 1px solid var(--border);
    font-size: 0.875rem;
}

.shared-people-list .shared-name { font-weight: 600; min-width: 10rem; }
.shared-people-list .shared-meta { color: var(--text-dim); }
.shared-people-list .shared-do { margin-left: auto; display: flex; flex-wrap: wrap; gap: 4px 12px; }
.shared-people-list li.retired .shared-name { text-decoration: line-through; color: var(--text-dim); }

/* ------------------------------------------------ the sign-in's end
 *
 * A minute before an idle sign-in ends: Still there?, and Stay signed in
 * (portal.js, "the sign-in's end"). At the foot of the window, over
 * everything - inside an open popup, which is the top layer, and above the
 * scan (200) and a map or chart opened to the full window (2000) - and out
 * of the way of what is being read; on a phone, the width of the screen. */
.signs-out-note {
    position: fixed;
    left: 50%;
    bottom: 16px;
    z-index: 2100;
    display: flex;
    align-items: center;
    gap: 12px 16px;
    width: max-content;
    max-width: calc(100vw - 32px);
    padding: 12px 14px 12px 18px;
    border: 1px solid var(--menu-border);
    border-radius: 12px;
    background: var(--menu-bg);
    box-shadow: var(--menu-shadow);
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 0.9375rem;
    line-height: 1.45;
    transform: translateX(-50%);
}

.signs-out-note[hidden] { display: none; }

.signs-out-note p { margin: 0; max-width: 34rem; }

.signs-out-note .action { flex: none; }

@media (max-width: 40rem) {
    .signs-out-note {
        flex-wrap: wrap;
        width: auto;
        left: 16px;
        right: 16px;
        transform: none;
    }

    .signs-out-note .action { width: 100%; justify-content: center; }
}

@media (pointer: coarse) {
    .signs-out-note .action { min-height: 44px; }
}
