/* --- District palette -----------------------------------------------------
   Taken from bolesisd.net: a black utility bar sitting over a hornet-green
   brand band, on white. Declared once as tokens because the alternative is the
   same nine hex values written out ninety times, and a district that rebrands
   would then need every one of them found.

   Three greens rather than one, because a colour that reads well as a wide band
   behind black text does not have the contrast to be link text on white. The
   band is --green; anything that has to be read as text on white is
   --green-deep, which clears WCAG AA against white at 7.9:1
   (docs/04-nonfunctional.md section 3, and OQ-9's Title II deadline). */
:root {
    --black: #000000;
    --ink: #1a1a1a;
    --muted: #4a4a4a;

    --green: #1a9d4a;
    --green-dark: #137a3c;
    --green-deep: #0b5f2d;
    --green-tint: #eef7f1;

    --warn: #8a6d0b;
    --warn-tint: #fdf6e3;
    --warn-ink: #6b5309;

    --danger: #a4001b;
    --danger-tint: #fdf0f2;

    --rule: #d0d0d0;
    --rule-light: #e0e0e0;
    --rule-faint: #ececec;

    /* The honeycomb behind the brand band — hornets, and the pattern the
       district's own header carries, traced off it and then set at two thirds
       of the size it is drawn there, which is what this shorter band takes:
       regular hexagons 33px across the flats and 38px point to point, standing
       on a vertex, in staggered rows 28.5px apart, ruled 3.7px wide. One tile
       is two rows, so 33x57, and the row offset is half a hexagon either way,
       which is what lets the tile close on all four sides. Whole pixels both
       ways, three thousandths off the regular hexagon's 1:1.1547 and no more:
       a tile with the exact ratio has a fractional height, and the browser
       resamples the repeat rather than laying it down on the pixel grid.

       One filled path rather than six stroked hexagons. Neighbouring cells
       share a wall, so stroking each cell in turn lays two passes of the same
       faint ink over every shared edge and three over every vertex, and the
       vertices come out as a grid of dots. Filling the tile and punching the
       cells out of it with evenodd leaves the wall between them exactly once:
       the cells are the hexagons inset by half the wall, and what is left over
       is the comb. Vertices widen into a node there, as they do on the
       district's band and in the thing itself.

       Black at 0.03 rather than white: the comb reads slightly darker than the
       green around it, not lighter. Low enough contrast either way to be
       texture rather than decoration competing with the wordmark. */
    --honeycomb: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='33' height='57' viewBox='0 0 33 57'%3E%3Cpath fill='%23000000' fill-opacity='0.03' fill-rule='evenodd' d='M0 0H33V57H0ZM0 -16.87L14.65 -8.435L14.65 8.435L0 16.87L-14.65 8.435L-14.65 -8.435ZM33 -16.87L47.65 -8.435L47.65 8.435L33 16.87L18.35 8.435L18.35 -8.435ZM16.5 11.63L31.15 20.065L31.15 36.935L16.5 45.37L1.85 36.935L1.85 20.065ZM0 40.13L14.65 48.565L14.65 65.435L0 73.87L-14.65 65.435L-14.65 48.565ZM33 40.13L47.65 48.565L47.65 65.435L33 73.87L18.35 65.435L18.35 48.565Z'/%3E%3C/svg%3E");
}

/* An app shell rather than a document that scrolls as a whole: the window holds
   the two header bands at their natural height and gives the rest to a single
   scrolling region below them.

   Two things fall out of that, both of them the point. The scrollbar belongs to
   the region under the header rather than to the window, so the black and green
   bands run edge to edge whether or not the page below is long enough to need
   one — a reserved gutter beside them is outside the document's content box and
   cannot be painted, which is what left a white strip down the right. And
   scrollbar-gutter on the scrolling region keeps its width the same either way,
   so nothing centred inside it moves sideways as you go between screens. */
html {
    height: 100%;
    overflow: hidden;
}

/* Graduate, the nearest open face to the collegiate block the district letters
   its name in: octagonal bowls, slab serifs on the stems, and near enough the
   same letter widths that the wordmark sets to the same length. Carried in the
   app's own wwwroot rather than fetched from a font CDN, which is the whole of
   what the note on .brand-name objected to — no third-party request on the
   login screen, and nothing to break when the district's filtering blocks a
   host. Latin subset, 6KB. SIL Open Font License; the text of it sits beside
   the file as fonts/graduate-OFL.txt. */
@font-face {
    font-family: "Graduate";
    src: url("fonts/graduate-latin.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;

    /* swap, so the wordmark is readable from the first paint and takes the
       district's lettering as soon as the file lands. 6KB from the app's own
       origin makes that window short enough not to be worth blocking the
       header for, and the alternative failure — a wordmark held blank while a
       missing file times out — is the worse one on a login screen. */
    font-display: swap;
}

/* The app had no font stack at all and was falling back to the browser default,
   which is a serif. Nothing on bolesisd.net is set in a serif, and neither is
   anything a school district hands to staff. */
body {
    height: 100%;
    display: flex;
    flex-direction: column;
    margin: 0;
    color: var(--ink);
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
        Arial, sans-serif;
    line-height: 1.5;
}

h1:focus {
    outline: none;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid var(--green);
}

.invalid {
    outline: 1px solid var(--danger);
}

.validation-message {
    color: var(--danger);
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}
/* --- Application shell and sign-in ---------------------------------------
   Two bands, as bolesisd.net has them: a black strip carrying the navigation
   over a green band carrying the wordmark. Structural rather than decorative —
   putting the nav on its own row is what stops it competing with the identity
   block for the same line, which is why the district's own header does it.

   What still matters more than any of it is that every control is keyboard
   reachable and its focus ring is visible against whatever it sits on, which
   the accessibility pass in docs/04-nonfunctional.md section 3 requires of
   every screen — hence the separate focus rules for the dark bands below. */

/* Both bands lay their contents out in the same container, so the wordmark and
   the first nav item share a left edge. Without it the two rows are centred
   independently and the header reads as two unrelated strips. */
/* The same width and gutter main.page.wide uses, so the wordmark and a page
   heading sit on one left edge rather than two that nearly agree. */
.band-inner {
    max-width: 80rem;
    margin: 0 auto;
    padding: 0 1.25rem;
}

/* The two bands keep their own height; the scrolling region takes what is
   left. */
.site-header {
    flex: none;
}

/* min-height:0 because a flex item will not shrink below its content by
   default, which would grow the shell past the window and put the scrollbar
   back on the document — the one thing this layout exists to avoid. */
.app-scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    scrollbar-gutter: stable;
}

/* --tab-pad is the horizontal padding every item in this bar carries, so that
   the current one can be a white tab rather than white text. It is a token
   because the list below has to subtract it back out of its own gutter and gap
   to keep this row's text on the same left edge as the wordmark. */
.top-nav {
    background: var(--black);
    --tab-pad: 0.75rem;
}

/* The list carries .band-inner, whose margin and gutter are what line this row
   up with the wordmark below it, so the gutter is not reset here — it is moved
   inwards by exactly the padding each item now carries, and the width grows by
   the same amount. That keeps this band's outer width equal to the brand band's,
   which is what keeps the two centred as one. The 80rem and 1.25rem are
   .band-inner's own numbers; a change there has to be made here as well. */
.top-nav ul {
    display: flex;
    flex-wrap: wrap;

    /* Neighbouring items still read 1.75rem apart: their two paddings now take
       all but a quarter of it. */
    gap: 0.25rem calc(1.75rem - 2 * var(--tab-pad));
    list-style: none;
    max-width: calc(80rem + 2 * var(--tab-pad));
    padding-inline: calc(1.25rem - var(--tab-pad));
}

.top-nav a {
    display: inline-block;
    padding: 0.7rem var(--tab-pad);
    color: #fff;
    font-weight: 700;
    font-size: 0.95rem;
    text-decoration: none;
    border-bottom: 3px solid transparent;
}

/* Hovered, or reached by keyboard: the item fills with the green the honeycomb
   band below it is painted on, and its label goes black for the same reason
   that band's does. White on this green is 3.5:1 and black is 6:1, and only one
   of those clears AA (docs/04-nonfunctional.md section 3). The underline goes
   under the fill, which is why the border is given the same green rather than
   left to show as a line across it.

   Kept above .active on purpose: the current screen holds its white tab as the
   pointer crosses it instead of flickering green. */
.top-nav a:hover,
.top-nav a:focus-visible {
    background: var(--green);
    color: var(--black);
    border-bottom-color: var(--green);
}

/* The current screen, lifted off the black bar as a white tab. Weight alone
   would not carry it — every item in this bar is already bold.

   The underline is white too, so the tab is one solid block: a green line
   across its foot reads as a second thing, and the tab has already said which
   screen this is. The border stays rather than going to none, because it is
   3px of the item's height and every other item in the row carries it. */
.top-nav a.active {
    background: #fff;
    color: var(--black);
    border-bottom-color: #fff;
}

/* The manual, held against the right-hand end of the bar. The gap between it
   and the last screen is the whole point: it is a document rather than a
   screen, and an admin's bar has three more items than a teacher's, so the
   distance cannot be a fixed one. A wrapped row leaves it alone on the right
   of the line below, which still reads as "and this one is different". */
.top-nav .nav-end {
    margin-left: auto;
}

/* White, because the district green has nothing like enough contrast against
   black to be seen as a focus ring on it. */
.top-nav a:focus-visible,
.brand-band a:focus-visible,
.brand-band button:focus-visible {
    outline: 2px solid #fff;
    outline-offset: 2px;
}

/* A white ring is invisible on the one item that is itself white, so the
   current tab gets a black one, drawn inside the block rather than out on the
   bar behind it. */
.top-nav a.active:focus-visible {
    outline-color: var(--black);
    outline-offset: -4px;
}

.brand-band {
    position: relative;
    z-index: 1;
    background-color: var(--green);
    background-image: var(--honeycomb);

    /* The tile's own size, spelled out, so the one number that sets how big
       the comb reads is in the rule that uses it rather than buried in the
       tile. */
    background-size: 33px 57px;

    /* Centred down the band rather than hung from its top. The band is 99px
       and the comb repeats every 57, so anchoring it at the top leaves the
       bottom edge slicing a row of cells apart while the top edge cuts cleanly
       — the pattern reads as having run out of room. Centred, the two edges cut
       the same way and a whole row of cells sits across the middle. It also
       holds when the band changes height: the phone breakpoint shrinks the
       hornet and takes the band down with it, and there is no number here to
       follow it. Horizontal stays at 0, so the comb does not slide sideways as
       the window is resized. */
    background-position: 0 50%;
    color: var(--black);
}

.brand-band .band-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    padding-top: 1.1rem;
    padding-bottom: 1.1rem;
}

.brand {
    display: inline-flex;
    align-items: center;
    gap: 0.7rem;
    text-decoration: none;
    color: inherit;
    line-height: 1;
}

/* Height-led, with width:auto overriding the intrinsic attributes so the aspect
   ratio survives if the artwork is ever recut. The width/height pair stays on the
   tag regardless: it is what reserves the space before the image arrives, and
   without it the wordmark jumps left on every cold load.

   The hornet is drawn taller than the band it sits on and hangs off the bottom of
   it. Two rules do that and both matter: flex-start pins its top to the band's own
   top padding, so it can never ride up into the black nav above; the negative
   bottom margin keeps the overhang out of the layout, so the band stays the height
   its wordmark asks for instead of growing to fit the artwork. The overhang is
   kept inside main's 2rem top margin so it lands on empty page rather than on a
   heading. */
.brand-mark {
    height: 7rem;
    width: auto;
    align-self: flex-start;
    margin-bottom: -3rem;
}

/* One line, baseline-aligned, so "Boles ISD", the dash and "Hive" read as a single
   wordmark rather than a name with a strapline under it. Baseline rather than
   centre because the italic is set at the same size as the roman and only the
   baseline keeps the two from drifting against each other. */
.brand-words {
    display: inline-flex;
    align-items: baseline;
    gap: 0.4rem;
    flex-wrap: wrap;
}

/* The district's own name, in the district's own lettering. The condensed
   system stack stays behind it: the app may be hosted on-premises (OQ-12), so
   the face is served from wwwroot and the fallback has to hold the line on its
   own if the file is slow or missing.

   400, not 800, because Graduate ships one weight and asking a browser for a
   heavier one it does not have gets a synthetic smear of the outline rather
   than a bolder cut. The weight instead comes from stroking the glyphs in
   their own colour, which the district's lettering is heavy enough to want:
   0.06em holds the counters open at 2.1rem and, being an em, comes down with
   the font on a phone. A browser that does not know the property draws the
   wordmark a little light, which is the whole of the damage.

   No letter-spacing: the 0.02em was there to loosen Arial Narrow, and Graduate
   is already spaced for this. */
.brand-name {
    font-family: "Graduate", "Arial Narrow", "Helvetica Neue Condensed",
        "Roboto Condensed", "Liberation Sans Narrow", system-ui, sans-serif;
    font-weight: 400;
    font-size: 2.1rem;
    text-transform: uppercase;
    -webkit-text-stroke: 0.06em currentColor;
}

.brand-dash,
.brand-tag {
    font-family: "Arial Narrow", "Helvetica Neue Condensed", "Roboto Condensed",
        "Liberation Sans Narrow", system-ui, sans-serif;
    font-weight: 800;
    font-size: 2.1rem;
    letter-spacing: 0.02em;
    text-transform: uppercase;
}

/* Italic is the whole distinction between the district's name and the
   application's, so it carries some weight: kept upright, "BOLES ISD HIVE" reads
   as one four-word name rather than as this district's Hive. */
.brand-tag {
    font-style: italic;
}

.brand-dash {
    font-weight: 400;
}

/* A phone cannot carry a 7rem hornet and a two-word wordmark on one line, so both
   come down a size together and the overhang shortens with them. */
@media (max-width: 40rem) {
    .brand-mark {
        height: 4.5rem;
        margin-bottom: -1.5rem;
    }

    .brand-name,
    .brand-dash,
    .brand-tag {
        font-size: 1.5rem;
    }
}

.identity {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    color: inherit;
    font-weight: 600;
}

.brand-band .who {
    color: inherit;
}

/* Black on green, as the district's own header has its controls. The generic
   button.link is a green that would vanish into the band. */
.brand-band button.link,
.brand-band a {
    color: var(--black);
}

a.identity {
    text-decoration: none;
    border-bottom: 2px solid var(--black);
}

button.link {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    color: var(--green-deep);
    text-decoration: underline;
    cursor: pointer;
}

main.auth {
    max-width: 26rem;
    margin: 3rem auto;
    padding: 0 1.25rem;
}

/* The sign-in page's own title carries the same green rule the brand band does,
   so the one screen a person meets before the header means anything to them
   still looks like the district's. */
main.auth h1 {
    padding-bottom: 0.5rem;
    border-bottom: 3px solid var(--green);
    font-size: 1.6rem;
}

main.auth label {
    display: block;
    margin-top: 0.6rem;
    font-weight: 600;
}

main.auth label.checkbox {
    font-weight: 400;
}

/* Matched on "not a checkbox" rather than on type="text", because InputText
   renders a bare <input> with no type attribute at all — so the username box
   was missing this rule while the password box, which passes type explicitly,
   picked it up, and the two ended up different widths. */
main.auth input:not([type="checkbox"]) {
    width: 100%;
    padding: 0.25rem 0.4rem;
    font: inherit;
    font-size: 0.9rem;
    box-sizing: border-box;
}

main.auth button[type="submit"] {
    margin-top: 1.5rem;
    padding: 0.5rem 1.25rem;
    font: inherit;
    cursor: pointer;
}

main.auth .error {
    padding: 0.75rem;
    border: 1px solid var(--danger);
    background: var(--danger-tint);
    color: var(--danger);
}

main.auth .hint {
    margin-top: 2rem;
    color: #555;
    font-size: 0.9rem;
}

/* "Check your email" after the address is sent. A rule down the side rather than
   a box, so it reads as where the person is up to rather than as a warning. */
main.auth .sent {
    margin-top: 1rem;
    padding: 0.25rem 0 0.25rem 0.75rem;
    border-left: 3px solid var(--green);
}

main.auth.check-email h2 {
    margin-top: 1.75rem;
    font-size: 1.15rem;
}

main.auth.check-email .sign-in-steps li + li {
    margin-top: 0.5rem;
}

/* Spaced digits, so a six-digit PIN can be checked against the email at a glance. */
main.auth input.pin {
    letter-spacing: 0.35em;
    font-variant-numeric: tabular-nums;
}

/* --- The sign-in chooser --------------------------------------------------
   Two cards rather than a form, because the first thing the screen asks is
   which kind of person is signing in, and what it has to collect after that
   differs by the answer. Each card is a link to the page that collects it.

   Wider than main.auth's own 26rem: two cards need the room side by side, and
   a single column of them stops reading as a choice between two things and
   starts reading as a list of links. */
main.auth.choose {
    max-width: 46rem;
    text-align: center;
}

/* The heading keeps the rule the rest of the app's headings carry, cut to the
   width of the word rather than run across the column — centred text under a
   full-width rule reads as a section divider, and this is a title.
   inline-block is what lets the border stop where the word stops.

   Gold rather than the band green, which is the one place this screen departs
   from the palette's usual division of labour: green is carrying the buttons
   and the two marks below, and a third green line between them would leave
   nothing on the screen to separate the district's welcome from its
   controls. */
main.auth.choose h1 {
    display: inline-block;
    border-bottom-color: var(--warn);
}

main.auth.choose .lede {
    margin: 1.25rem 0 0;
    font-size: 1.1rem;
    font-weight: 700;
}

main.auth.choose .sub {
    margin: 0.35rem 0 0;
    color: var(--muted);
}

/* Two equal columns that collapse to one before the cards get too narrow to
   hold "Continue as Staff" and its arrow on one line. */
main.auth.choose .choices {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1.25rem;
    margin: 2rem 0 0;
    padding: 0;
    list-style: none;
}

@media (max-width: 34rem) {
    main.auth.choose .choices {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* One card while guest access is off: centred at the width it has beside a second one, rather
   than stretched across both columns. */
main.auth.choose .choices.single {
    grid-template-columns: minmax(0, 22rem);
    justify-content: center;
}

/* A column, so the button can be pushed to the bottom with margin-top:auto.
   The two cards are the same height whatever their text does — grid stretches
   them — and without that the buttons sit at two different heights, which is
   what makes a pair of choices look like one is an afterthought. */
.choice {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    padding: 1.75rem 1.5rem;
    border: 1px solid var(--rule-light);
    border-radius: 10px;
    background: #fff;
    box-shadow: 0 1px 3px rgb(0 0 0 / 0.08);
}

.choice-mark {
    display: grid;
    place-items: center;
    width: 4.25rem;
    height: 4.25rem;
    border-radius: 50%;
    background: var(--green-deep);
    color: #fff;
}

/* Sized here rather than on the tags, so the two drawings — one of three
   people, one of one — come out the same size as each other. */
.choice-mark svg {
    width: 2.1rem;
    height: 2.1rem;
}

.choice h2 {
    margin: 0.25rem 0 0;
    padding-bottom: 0.5rem;
    border-bottom: 3px solid var(--warn);
    font-size: 1.25rem;
}

.choice p {
    margin: 0;
    color: var(--muted);
}

/* An anchor wearing the primary button's colours: it navigates rather than
   submitting, and a real button here would need script to do the one thing a
   link does for free. The label and the arrow are pushed to opposite ends, so
   both cards' arrows line up down the right whatever the labels are. */
.choice-go {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    box-sizing: border-box;
    width: 100%;
    margin-top: auto;
    padding: 0.7rem 1.1rem;
    border: 1px solid var(--green-dark);
    border-radius: 4px;
    background: var(--green-dark);
    color: #fff;
    font-weight: 600;
    text-decoration: none;
}

.choice-go:hover {
    background: var(--green-deep);
    border-color: var(--green-deep);
}

.choice-go:focus-visible {
    outline: 2px solid var(--green-deep);
    outline-offset: 2px;
}

main.auth.choose .auth-foot {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.9rem;
    margin-top: 2.5rem;
    color: var(--muted);
    font-size: 0.9rem;
}

main.auth.choose .secure {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
}

main.auth.choose .secure svg {
    width: 1.05rem;
    height: 1.05rem;
}

/* A drawn divider rather than a typed "|", which a screen reader reads out as
   a word and which sits on the text baseline instead of centred on the row. */
main.auth.choose .auth-foot-rule {
    width: 1px;
    height: 1.1rem;
    background: var(--rule);
}

/* --- Scheduling screens -------------------------------------------------- */

/* Every screen is as wide as the equipment catalog, and as the header band above it (.band-inner), so
   the wordmark and a page's heading share one left edge and a wide screen is not mostly gutter. */
main.page {
    max-width: 80rem;
    margin: 2rem auto;
    padding: 0 1.25rem;
}

/* The single-column forms used to be narrower still. They take the full width now, but a text box
   stretched across it stops being readable, so the long-text fields keep a line length. */
main.page.narrow textarea {
    max-width: 42rem;
}

main.page .field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    margin-bottom: 1rem;
}

main.page label {
    font-weight: 600;
}

main.page label.checkbox {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-weight: 400;
    margin-right: 1rem;
}

main.page input,
main.page select,
main.page textarea {
    font: inherit;
    font-size: 0.9rem;
    padding: 0.2rem 0.4rem;
}

/* A date or time input is taller than a dropdown by the height of its picker icon, so
   a row of mixed filters sat ragged. One height for all of them. */
main.page input:not([type="checkbox"], [type="radio"], [type="file"]),
main.page select,
main.auth.portal select {
    box-sizing: border-box;
    height: 2rem;
}

main.page textarea {
    width: 100%;
}

/* Wraps rather than scrolls: the filter row is the first thing to overflow on a
   phone, and a horizontal scrollbar hides the filter nobody knew was there. */
.filters {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    align-items: flex-start;
}

/* A second row of filters is a second question, not more of the first: the gap between the two
   is the row gap the fields inside them already sit on. */
.filters + .filters {
    margin-top: 1rem;
}

/* A label over a row of filters so it is clear what the dates and times in it narrow. */
/* Setup by and Event side by side with a thin rule between them. Each takes half the row and its four
   boxes shrink to fit; below a tablet's width they stack, and the rule moves above the second. */
.filter-groups {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    margin-top: 1rem;
}

.filter-groups > .filter-group {
    flex: 1 1 24rem;
    min-width: 0;
    margin-top: 0;
}

.filter-groups > .filter-group + .filter-group {
    padding-left: 1rem;
    border-left: 1px solid var(--rule);
}

.filter-groups .filters .field {
    flex: 1 1 6.5rem;
    min-width: 0;
}

.filter-groups .filters input {
    width: 100%;
    box-sizing: border-box;
}

@media (max-width: 62rem) {
    .filter-groups > .filter-group + .filter-group {
        padding-left: 0;
        padding-top: 1rem;
        border-left: 0;
        border-top: 1px solid var(--rule);
    }
}

.filter-group {
    margin-top: 1rem;
}

.filter-group-label {
    margin: 0 0 0.4rem;
    font-size: 1.05rem;
    font-weight: 700;
}

.features {
    border: 1px solid var(--rule);
    padding: 0.75rem 1rem;
    margin: 0 0 1rem;
}

.zone-note,
.aside {
    color: #555;
    font-size: 0.9rem;
}

main.page button[type="submit"] {
    margin-top: 1rem;
    padding: 0.5rem 1.25rem;
    font: inherit;
    cursor: pointer;
}

.actions {
    display: flex;
    align-items: center;
    gap: 1.5rem;
}

/* Buttons pushed to the right edge, as the approval form has them. */
.actions.actions-end {
    justify-content: flex-end;
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--rule);
}

/* The one button in an end-aligned row that stays at the start. */
.actions-end > .deny-first {
    margin-right: auto;
}

/* A form's Save and Cancel sit on one line at one height. The submit button carries its own top margin,
   which pushed it below its neighbour; the gap above the row is the row's instead. */
div.actions {
    margin-top: 1rem;
    align-items: stretch;
}

main.page .actions button[type="submit"],
main.auth .actions button[type="submit"] {
    margin-top: 0;
}

main.page .actions button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

/* --- Buttons --------------------------------------------------------------
   Filled with --green-dark rather than the band's --green: white text on the
   band green is 3.6:1 and fails AA, and a button nobody can read is not a
   brand decision. The darker green clears it at 5.4:1. */
main.page button[type="submit"],
main.auth button[type="submit"] {
    background: var(--green-dark);
    border: 1px solid var(--green-dark);
    color: #fff;
    font-weight: 600;
}

main.page button[type="submit"]:hover:not(:disabled),
main.auth button[type="submit"]:hover:not(:disabled) {
    background: var(--green-deep);
    border-color: var(--green-deep);
}

main.page button[type="submit"]:disabled,
main.auth button[type="submit"]:disabled {
    background: var(--muted);
    border-color: var(--muted);
    cursor: default;
}

/* The secondary ones — "Add equipment", "Check out", the things that reveal a
   form rather than submit it. Outlined so the page has one obvious primary
   action per form and these do not compete with it. */
main.page button[type="button"]:not(.link) {
    padding: 0.4rem 0.9rem;
    font: inherit;
    background: #fff;
    border: 1px solid var(--green-dark);
    color: var(--green-deep);
    font-weight: 600;
    cursor: pointer;
}

main.page button[type="button"]:not(.link):hover:not(:disabled) {
    background: var(--green-tint);
}

main.page button[type="button"]:not(.link):disabled {
    border-color: var(--rule);
    color: var(--muted);
    cursor: default;
}

/* Deny and Cancel: the actions that turn something down or take it back. The danger red on its
   tint, so they read as the opposite of the green ones beside them. */
main.page button[type="button"].danger,
main.page a.danger {
    padding: 0.4rem 0.9rem;
    font: inherit;
    background: var(--danger-tint);
    border: 1px solid var(--danger);
    color: var(--danger);
    font-weight: 600;
    cursor: pointer;
    display: inline-block;
    text-decoration: none;
}

main.page button[type="button"].danger:hover:not(:disabled),
main.page a.danger:hover {
    background: #f9d9de;
}

main.page button[type="button"].danger:disabled {
    border-color: var(--rule);
    color: var(--muted);
    background: none;
    cursor: default;
}

/* A Cancel that only closes a form: black on grey, so it does not read as a destructive action. */
main.page button[type="button"].neutral {
    padding: 0.4rem 0.9rem;
    font: inherit;
    background: #eee;
    border: 1px solid #555;
    color: #111;
    font-weight: 600;
    cursor: pointer;
}

main.page button[type="button"].neutral:hover:not(:disabled) {
    background: #ddd;
}

main.page button[type="button"].neutral:disabled {
    border-color: var(--rule);
    color: var(--muted);
    background: none;
    cursor: default;
}

/* Update: saves without deciding. Dark blue on light blue, apart from the green yes and the red no. */
main.page button[type="button"].info {
    padding: 0.4rem 0.9rem;
    font: inherit;
    background: #dbeafe;
    border: 1px solid #1e3a8a;
    color: #1e3a8a;
    font-weight: 600;
    cursor: pointer;
}

main.page button[type="button"].info:hover:not(:disabled) {
    background: #bfdbfe;
}

main.page button[type="button"].info:disabled {
    border-color: var(--rule);
    color: var(--muted);
    background: none;
    cursor: default;
}

/* Each action in the colour of what it does. Approve is the filled green, the one yes; Mark in review is
   amber, work started but not answered; Details is grey, it only opens things. Update is blue and Deny and
   Cancel booking are red, above. */
main.page button[type="button"].go {
    padding: 0.4rem 0.9rem;
    font: inherit;
    background: var(--green-dark);
    border: 1px solid var(--green-dark);
    color: #fff;
    font-weight: 600;
    cursor: pointer;
}

main.page button[type="button"].go:hover:not(:disabled) {
    background: var(--green-deep);
    border-color: var(--green-deep);
}

main.page button[type="button"].go:disabled {
    background: var(--muted);
    border-color: var(--muted);
    cursor: default;
}

main.page button[type="button"].caution {
    padding: 0.4rem 0.9rem;
    font: inherit;
    background: var(--warn-tint);
    border: 1px solid var(--warn);
    color: var(--warn-ink);
    font-weight: 600;
    cursor: pointer;
}

main.page button[type="button"].caution:hover:not(:disabled) {
    background: #f7ecc4;
}

main.page button[type="button"].caution:disabled {
    border-color: var(--rule);
    color: var(--muted);
    background: none;
    cursor: default;
}

/* The office's notes on a request, under its buttons. */
.request-notes-block {
    margin-top: 1.25rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--rule);
}

.notes-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 1rem;
    margin-bottom: 0.4rem;
}

/* The small box under the latest note that says older ones are behind it. */
main.page button.more-notes {
    margin: 0.6rem 0 0;
    padding: 0.1rem 0.6rem;
    font: inherit;
    font-size: 0.8rem;
    font-weight: 600;
    background: #eee;
    border: 1px solid #999;
    border-radius: 999px;
    color: #333;
    cursor: pointer;
}

main.page button.more-notes:hover {
    background: #ddd;
}

/* The "+" under the notes that opens the box to write a new one. */
main.page button.add-note {
    margin: 0;
    padding: 0 0.7rem;
    font: inherit;
    font-size: 1.2rem;
    font-weight: 700;
    line-height: 1.6;
    background: var(--green-tint);
    border: 1px solid var(--green-dark);
    border-radius: 0.3rem;
    color: var(--green-deep);
    cursor: pointer;
}

main.page button.add-note:hover {
    background: #d9eed9;
}

/* A section of the approval form: a rule above it, then its bold heading, the same weight as Notes. */
.form-section {
    margin-top: 1.25rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--rule);
}

/* What was asked for and is no longer what stands, kept readable but crossed out. */
/* An approved value that is not what was requested: bold on an amber band, so it stands out against the
   crossed-out request above it. */
.changed {
    padding: 0.1rem 0.4rem;
    background: #ffe69c;
    border-radius: 0.25rem;
    font-weight: 700;
    color: #3d2f00;
}

.struck {
    text-decoration: line-through;
    color: var(--muted);
}

.section-label {
    margin: 0 0 0.5rem;
    font-size: 1.05rem;
    font-weight: 700;
}

label.section-label {
    display: block;
}

/* The label at the head of the notes. */
.notes-label {
    margin: 0;
    font-size: 1.05rem;
}

/* Something the reader should know, as against something that went wrong. The
   district green rather than the error red, because a red box says "you have a
   problem" and these say "here is how this page is behaving". */
.notice {
    padding: 0.6rem 0.9rem;
    border-left: 4px solid var(--green-dark);
    background: var(--green-tint);
    margin: 1rem 0;
}

.notice.warn {
    border-left-color: #e0b400;
    background: var(--warn-tint);
}

/* A green rule under the page title, echoing the band above it. Decorative, so
   it is a border rather than a character anything would read out. */
main.page > h1 {
    padding-bottom: 0.4rem;
    border-bottom: 3px solid var(--green);
}

main.page h2 {
    margin-top: 2rem;
}

.results {
    list-style: none;
    padding: 0;
    display: grid;
    gap: 1rem;
}

.room {
    border: 1px solid var(--rule);
    border-left: 4px solid var(--green-dark);
    padding: 0.75rem 1rem;
}

/* Colour is not the only signal — a busy room also states its conflicts in text,
   because a left border is invisible to a screen reader and to anyone who cannot
   distinguish it from the free one. */
.room.busy {
    border-left-color: #8a8a8a;
}

.room-head {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
    flex-wrap: wrap;
}

.room h4 {
    margin: 0;
    font-size: 1.05rem;
}

/* --- Admin tiles and breadcrumbs ------------------------------------------
   The locations admin is a drill-down — campuses, then buildings, then floors,
   then rooms — so each level is a grid of tiles linking one level deeper, in
   the card shape find-a-room already uses for its results. The equipment admin
   is the same walk down categories, and shares this rather than growing a
   second card that is nearly but not quite the same. */
.crumbs {
    margin: 0 0 0.5rem;
    font-size: 0.9rem;
    color: #555;
}

.crumb-sep {
    margin: 0 0.4rem;
    color: #999;
}

.tiles {
    list-style: none;
    padding: 0;
    margin: 1rem 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: 1rem;
}

.tiles .tile {
    display: block;
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    text-align: left;
    text-decoration: none;
    color: var(--ink);
    background: #fff;
    border: 1px solid var(--rule);
    border-left: 4px solid var(--green-dark);
    padding: 0.75rem 1rem;
}

.tiles .tile:hover,
.tiles .tile:focus-visible {
    background: var(--green-tint);
}

/* The category a card came out of, on a list that spans several. Above the
   name and quieter than it: it says which shelf this was on, not what it is. */
.tile-kind {
    display: block;
    font-size: 0.75rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: #666;
    margin-bottom: 0.15rem;
}

.tile-name {
    display: block;
    font-weight: 600;
    margin-bottom: 0.25rem;
}

.tiles .tile .facts {
    display: block;
    color: #555;
    font-size: 0.9rem;
}

/* Retired is stated in a badge as well as in the grey rule: a left border is
   invisible to a screen reader and to anyone who cannot pick it out. */
.tiles .tile.retired {
    border-left-color: #8a8a8a;
    background: #fafafa;
}

/* The add tile is a button, so it has to be talked out of the filled style the
   page gives every other one. Dashed, to read as a gap waiting to be filled. */
main.page .tiles button.tile.new {
    font: inherit;
    cursor: pointer;
    border: 1px dashed var(--green-dark);
    color: var(--green-deep);
    background: #fff;
}

main.page .tiles button.tile.new:hover:not(:disabled),
main.page .tiles button.tile.new:focus-visible {
    background: var(--green-tint);
    border: 1px dashed var(--green-dark);
}

/* When a piece of equipment is already spoken for. A plain list rather than a
   grid: it is read top to bottom, looking for the first date that is not there. */
.booked h2 {
    margin-bottom: 0.25rem;
}

.periods {
    list-style: none;
    padding: 0;
    margin: 0 0 0.5rem;
}

.periods li {
    padding: 0.3rem 0;
    border-bottom: 1px solid var(--rule);
    font-variant-numeric: tabular-nums;
}

.periods li:last-child {
    border-bottom: 0;
}

.badge {
    border: 1px solid var(--warn);
    background: var(--warn-tint);
    color: var(--warn-ink);
    padding: 0.1rem 0.4rem;
    font-size: 0.8rem;
    border-radius: 3px;
}

.badge-line {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.room .where,
main.page .where {
    margin: 0.25rem 0;
    color: #444;
}

.room .facts,
main.page .facts {
    margin: 0.25rem 0;
    color: #555;
    font-size: 0.9rem;
}

.conflicts {
    margin: 0.5rem 0;
    padding-left: 1.2rem;
    font-size: 0.9rem;
}

.conflicts .when {
    color: #444;
    margin-right: 0.4rem;
}

.alternative {
    margin: 0.5rem 0 0;
}

.alternative a {
    margin-left: 0.75rem;
}

.conflict-preview {
    border: 1px solid var(--warn);
    background: var(--warn-tint);
    padding: 0.75rem 1rem;
    margin-bottom: 1rem;
}

.override {
    border: 1px solid var(--danger);
    padding: 0.75rem 1rem;
    margin: 1rem 0;
}

.confirmation {
    border: 1px solid var(--green-dark);
    padding: 1rem;
}

.warnings {
    margin: 0.5rem 0 0;
    padding-left: 1.2rem;
}

main.page .error {
    padding: 0.75rem;
    border: 1px solid var(--danger);
    background: var(--danger-tint);
    color: var(--danger);
}

/* --- Booking detail and the dashboard list ------------------------------- */

/* A description list rather than a table: these are one booking's fields, not
   rows of anything, and a screen reader announces the pairing without the
   header cells a table would need inventing. */
.detail {
    display: grid;
    grid-template-columns: minmax(8rem, max-content) 1fr;
    gap: 0.5rem 1.5rem;
    margin: 1.5rem 0;
}

.detail dt {
    font-weight: 600;
}

.detail dd {
    margin: 0;
}

.equipment {
    padding-left: 1.2rem;
}

.room .when {
    margin: 0.25rem 0;
    font-weight: 600;
}

.room h4 a {
    color: inherit;
}

/* The status badge reuses .badge's shape and only moves the colour, so a status
   nobody has styled still reads as a badge rather than as bare text. Every one
   of them also states itself in words — the colour is never the only signal. */
.badge.ok {
    border-color: var(--green-dark);
    background: #f1f8f1;
    color: #1e5620;
}

.badge.gone {
    border-color: #8a8a8a;
    background: #f4f4f4;
    color: #444;
}

/* Never remove the focus ring without replacing it; keyboard-only operation is
   a build requirement, not a preference. */
:where(a, button, input, select, textarea):focus-visible {
    outline: 2px solid var(--green-deep);
    outline-offset: 2px;
}

/* --- Month calendar (FR-2) ----------------------------------------------- */

/* Announced but not drawn. The grid carries a lot of meaning in position and
   shading, and each of those needs a words-only equivalent for a screen reader
   (docs/04-nonfunctional.md section 3). Clip rather than display:none, which
   would take it out of the accessibility tree along with the layout. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* Seven columns of readable text needs more than the 60rem the forms use. */
main.page.wide {
    max-width: 80rem;
}

.month-nav {
    display: flex;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
    margin: 1rem 0 0.5rem;
}

.month-nav h2 {
    margin: 0;
    min-width: 12rem;
}

.month-nav button {
    font: inherit;
    padding: 0.4rem 0.75rem;
    cursor: pointer;
}

table.month {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
    margin-top: 0.5rem;
}

table.month th {
    padding: 0.4rem;
    text-align: left;
    font-size: 0.85rem;
    color: #444;
    border-bottom: 1px solid var(--rule);
}

table.month td {
    vertical-align: top;
    border: 1px solid var(--rule-light);
    padding: 0.3rem;

    /* A tall row is better than a scrolling one: a day cell that scrolls hides
       bookings behind a gesture nobody makes on a calendar. */
    min-height: 6rem;
    height: 6rem;
}

/* The padding days either side of the month. Dimmed rather than blanked —
   a booking on the 1st that spills back into the 31st still has to be legible. */
table.month td.outside {
    background: #fafafa;
}

table.month td.outside .daynum {
    color: #999;
}

table.month td.today {
    outline: 2px solid var(--green-deep);
    outline-offset: -2px;
}

table.month td {
    position: relative;
}

table.month td:hover,
table.month td:focus-within {
    outline: 4px solid var(--green-deep);
    outline-offset: -4px;
}

a.add-request {
    position: absolute;
    top: 0.35rem;
    right: 0.35rem;
    z-index: 2;
    visibility: hidden;
    display: block;
    width: 1.6rem;
    height: 1.6rem;
    line-height: 1.5rem;
    text-align: center;
    font-size: 1.3rem;
    font-weight: 700;
    border-radius: 50%;
    background: var(--green-deep);
    color: #fff;
    text-decoration: none;
}

table.month td:hover a.add-request,
table.month td:focus-within a.add-request {
    visibility: visible;
}

.daynum {
    margin: 0 0 0.25rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: #333;
}

.chips {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
}

.chip {
    border-left: 3px solid var(--green-deep);
    background: #f2f6fd;
    font-size: 0.8rem;
    line-height: 1.3;
}

.chip.pending {
    border-left-color: var(--warn);
    background: var(--warn-tint);
}

.chip.draft {
    border-left-color: #8a8a8a;
    background: #f4f4f4;
}

/* Denied, cancelled and completed bookings, which only appear when the Status filter asks for them.
   Muted and dashed: they are a record, and the room is not held. The badge says which. */
.chip.ended {
    border-left-style: dashed;
    border-left-color: #b4b4b4;
    background: #fafafa;
    color: var(--muted);
}

/* Approved bookings by when they happen: magenta while on, charcoal and faded once over. The badge
   says "Happening now" or "Finished" as well. */
.chip.live {
    border-left-color: #d6249f;
    border-left-width: 5px;
    background: #fdf0f8;
}

.chip.past {
    border-left-color: #1f2937;
    background: #f5f5f6;
    color: var(--muted);
}

/* A booking that carries on past this cell. Squaring the edge it continues
   through is a hint, not the message — the chip also says "from 3:00 PM" or
   "all day" in words. */
.chip.from-earlier {
    border-left-style: dashed;
}

.chip.into-later {
    border-right: 3px dashed var(--green-deep);
}

.chip-body {
    display: block;
    padding: 0.2rem 0.35rem;
    color: inherit;
    text-decoration: none;
}

a.chip-body:hover,
a.chip-body:focus-visible {
    text-decoration: underline;
}

.chip .when {
    display: block;
    font-weight: 600;
}

.chip .what {
    display: block;
}

.chip .where {
    display: block;
    color: #444;
    font-size: 0.75rem;
    margin: 0;
}

.chip .badge {
    display: inline-block;
    margin-top: 0.15rem;
    font-size: 0.7rem;
}

/* FR-2: the setup and teardown span, distinguished from the booking itself.
   Stated in words on the chip as well, because this shading is a colour
   difference and colour alone is not an accessible distinction. */
.chip .held {
    display: block;
    margin-top: 0.15rem;
    padding: 0.05rem 0.25rem;
    background: repeating-linear-gradient(
        135deg, #e6e6e6, #e6e6e6 3px, #f4f4f4 3px, #f4f4f4 6px);
    color: #333;
    font-size: 0.7rem;
}

/* Seven columns do not fit a phone. Below that the grid becomes one column of
   labelled days, which is the same information in the order it is read. */
@media (max-width: 40rem) {
    table.month,
    table.month tbody,
    table.month tr,
    table.month td {
        display: block;
        width: auto;
    }

    table.month thead {
        display: none;
    }

    table.month td {
        height: auto;
        min-height: 0;
        margin-bottom: 0.25rem;
    }

    /* With the column headings gone the day number has to carry the weekday. */
    .daynum::before {
        content: attr(data-full);
        font-weight: 600;
    }
}

/* --- Equipment catalog (FR-23) ------------------------------------------- */

/* Same card as a room result, and deliberately so: a person scanning for a TV
   cart is doing the same thing they do scanning for a room, and two different
   card shapes would make them learn the page twice. */
.gear {
    border: 1px solid var(--rule);
    border-left: 4px solid var(--green-dark);
    padding: 0.75rem 1rem;
}

/* Colour is not the only signal — every card also states its availability in
   words, for the reason .room.busy gives. */
.gear.busy {
    border-left-color: #8a8a8a;
}

.gear h4 {
    margin: 0;
    font-size: 1.05rem;
}

.gear .where,
.gear .facts {
    margin: 0.25rem 0;
}

.gear .stock {
    margin: 0.4rem 0 0;
    font-weight: 600;
}

.gear.busy .stock {
    font-weight: 500;
    color: #555;
}

/* --- Equipment inventory (FR-32) ----------------------------------------- */

.inventory {
    margin: 1.5rem 0;
}

.inventory h2 {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    flex-wrap: wrap;
}

/* Row actions sit inline and wrap rather than stacking, so a category with a
   dozen items is still one screen rather than three. */
.inventory .actions {
    display: flex;
    gap: 1rem;
    flex-wrap: wrap;
    margin: 0.4rem 0 0;
}

/* --- Day and week views (FR-2) ------------------------------------------- */

/* The view switcher. Links rather than buttons because each view is a URL, and
   a day you cannot send to somebody is half a day view. */
.views {
    display: flex;
    gap: 0.25rem;
    margin: 0.5rem 0;
}

.views a {
    padding: 0.35rem 0.9rem;
    border: 1px solid var(--rule);
    color: var(--green-deep);
    text-decoration: none;
}

.views a.active {
    background: var(--green-deep);
    border-color: var(--green-deep);
    color: #fff;
}

/* The date navigator's arrows are links now, for the same reason. Styled to
   match the buttons they replaced so the row does not change shape. */
.month-nav a.button {
    font: inherit;
    padding: 0.4rem 0.75rem;
    border: 1px solid var(--rule);
    color: var(--green-deep);
    text-decoration: none;
}

.month-nav a.button:hover,
.month-nav a.button:focus-visible,
.month-nav a.link:hover,
.month-nav a.link:focus-visible {
    text-decoration: underline;
}

/* Scrolls sideways rather than squeezing: a week of seven columns is readable
   on a laptop, and a day of fourteen rooms is not going to be, so it is given
   room and allowed to overflow into a scroll the reader can see the edge of. */
.timegrid {
    overflow-x: auto;
    margin-top: 0.5rem;
}

.timegrid-table {
    width: 100%;
    min-width: 44rem;
    border-collapse: collapse;
    table-layout: fixed;
}

.timegrid-table th {
    padding: 0.4rem;
    text-align: left;
    font-size: 0.85rem;
    color: #444;
    border-bottom: 1px solid var(--rule);
    vertical-align: top;
}

.timegrid-table thead th:first-child {
    width: 4.5rem;
}

.timegrid-table th.today,
.timegrid-table td.today {
    background: var(--green-tint);
}

.col-head {
    display: block;
    font-weight: 600;
    color: #222;
}

.col-sub {
    display: block;
    font-weight: 400;
}

.timegrid-table td {
    border-left: 1px solid var(--rule-light);
    padding: 0;
    vertical-align: top;
}

/* The whole grid is one row, and everything inside is positioned against it.
   Its height is the hour count times a fixed row, set from the markup: a
   percentage of a table cell has nothing definite to resolve against, so the
   rows would collapse to their content and the blocks would land on top of one
   another. 2.75rem an hour fits a time and a title without a scrollbar. */
.timegrid {
    --row: 2.75rem;
}

.timegrid-table .column,
.timegrid-table .hours {
    position: relative;
    height: calc(var(--hours, 12) * var(--row));
}

.hours {
    border-left: 0;
    padding: 0 !important;
}

/* One fixed row each, the same one the columns are sized in, so the labels and
   the lines beside them cannot drift apart. */
.hours .hour,
.hour-line {
    display: block;
    height: var(--row);
    box-sizing: border-box;
}

.hours .hour {
    font-size: 0.75rem;
    color: #555;

    /* Nudged up so a label sits against the line it names rather than floating
       in the middle of the hour after it. */
    margin-top: -0.4rem;
}

.hour-line {
    border-top: 1px solid var(--rule-faint);
}

/* Blocks are placed by percentage of the band, so the grid keeps its
   proportions at any height the stylesheet gives it. */
.block {
    position: absolute;
    box-sizing: border-box;
    border: 1px solid var(--green-dark);
    border-left: 3px solid var(--green-dark);
    background: #eef7ee;
    overflow: hidden;
    font-size: 0.78rem;
    line-height: 1.25;
}

.block.pending {
    border-color: var(--warn);
    border-left-color: var(--warn);
    background: var(--warn-tint);
}

.block.draft {
    border-color: #8a8a8a;
    border-left-color: #8a8a8a;
    background: #f4f4f4;
}

.block.ended {
    border-style: dashed;
    border-color: #b4b4b4;
    background: #fafafa;
    color: var(--muted);
}

.block.ended .block-booking {
    background: rgba(90, 90, 90, 0.08);
}

.block.live {
    border-color: #d6249f;
    border-left-width: 5px;
    background: #fdf0f8;
}

.block.live .block-booking {
    background: rgba(214, 36, 159, 0.16);
}

.block.past {
    border-color: #1f2937;
    background: #f5f5f6;
    color: var(--muted);
}

.block.past .block-booking {
    background: rgba(31, 41, 55, 0.08);
}

/* A booking that carries on past this column. A hint, not the message — the
   block also says "from 3:00 PM" or "all day" in words. */
.block.from-earlier {
    border-top-style: dashed;
}

.block.into-later {
    border-bottom-style: dashed;
}

/* FR-2's second clause: the block is the whole time the room is held, and this
   marks the booking itself inside it. The difference is shading, so the block
   says "Room held 1:30 PM – 3:30 PM" in words as well. */
.block-booking {
    position: absolute;
    left: 0;
    right: 0;
    background: rgba(19, 122, 60, 0.18);
    pointer-events: none;
}

.block.pending .block-booking {
    background: rgba(138, 109, 11, 0.18);
}

.block.draft .block-booking {
    background: rgba(90, 90, 90, 0.14);
}

.block-body {
    position: relative;
    display: block;
    padding: 0.15rem 0.3rem;
    color: inherit;
    text-decoration: none;
}

a.block-body:hover,
a.block-body:focus-visible {
    text-decoration: underline;
}

.block .when {
    display: block;
    font-weight: 600;
}

.block .what {
    display: block;
}

.block .where,
.block .who {
    display: block;
    color: #444;
    font-size: 0.72rem;
}

.block .badge {
    display: inline-block;
    margin-top: 0.15rem;
    font-size: 0.68rem;
}

.block .held {
    display: block;
    margin-top: 0.15rem;
    color: #555;
    font-size: 0.68rem;
}

/* A third of a column is not four lines wide. See TimeBlock.CompactClass — the
   text dropped here is still in the block's screen-reader description. */
.block.compact .who,
.block.compact .held,
.block.compact .badge {
    display: none;
}

.block.compact .block-body {
    padding: 0.1rem 0.2rem;
}

.block.compact .when {
    font-size: 0.7rem;
}

/* --- Hours and closures (FR-46) ------------------------------------------ */

.hours-grid {
    border-collapse: collapse;
    margin: 0.5rem 0 1rem;
}

.hours-grid th,
.hours-grid td {
    padding: 0.3rem 0.75rem 0.3rem 0;
    text-align: left;
    font-weight: 400;
}

.hours-grid thead th {
    font-weight: 600;
    font-size: 0.85rem;
    color: var(--muted);
    border-bottom: 1px solid var(--rule);
}

.hours-grid tbody th {
    font-weight: 600;
}

/* A disabled time box still has to be legible — it is showing what the hours
   would be if the day were set, and greying it into illegibility would make the
   row unreadable rather than merely inactive. */
.hours-grid input:disabled {
    color: var(--muted);
    background: #f7f7f7;
}

/* FR-46's question. Bordered in the warning colour rather than the error red:
   nothing has gone wrong, and nothing has been written — the admin is being
   asked to decide, which is a different thing from being told they were
   refused. */
.decision {
    border: 2px solid var(--warn);
    background: var(--warn-tint);
    padding: 1rem 1.25rem;
    margin: 1.5rem 0;
}

.decision h2 {
    margin-top: 0;
}

.affected {
    margin: 0.75rem 0;
    padding-left: 1.2rem;
}

.affected li {
    margin-bottom: 0.4rem;
}

.affected .aside {
    display: block;
}

/* --- People and roles (FR-47) --------------------------------------------- */

.people-filters {
    display: flex;
    align-items: flex-end;
    gap: 1.5rem;
    flex-wrap: wrap;
    margin: 1rem 0;
}

.people-filters .field {
    margin: 0;
}

/* A person's card holds its own buttons, so it is a div in the tile grid rather
   than a link, and it does not take the hover fill a whole-card link would. */
.tiles.person-tiles {
    grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
}

.tiles .tile.person:hover {
    background: #fff;
}

/* Deactivated cards are dimmed rather than hidden — this screen's job includes
   bringing somebody back, which needs them legible (INV-10). */
.tile.person.retired .tile-name {
    color: var(--muted);
}

.tile.person .facts {
    overflow-wrap: anywhere;
}

.tile.person .grants {
    margin: 0.4rem 0;
}

.card-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 1rem;
    margin-top: 0.5rem;
}

.grants {
    list-style: none;
    margin: 0;
    padding: 0;
}

.grants li {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    margin-bottom: 0.3rem;
}

/* Admin is the grant that hands out every other grant, so it does not share the
   colour of the ones it can issue. */
.badge.admin {
    background: var(--warn-tint);
    color: var(--warn-ink);
    border-color: var(--warn);
}

/* Big, monospaced and selectable: it is read down a phone or typed from the
   screen once, and a transcription error costs another reset. */
.issued-password {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 1.25rem;
    letter-spacing: 0.02em;
    user-select: all;
}

.fixed-value {
    margin: 0.2rem 0;
    font-weight: 600;
}

/* --- The staff portal's requests (FR-60) ----------------------------------
   The one screen in the application a person reaches with no account, so it
   borrows main.auth's column rather than the app shell: there is no nav to sit
   under, nothing to navigate to, and the header a signed-in user carries would
   offer links every one of which would refuse them.

   Wider than main.auth's 26rem all the same. That width suits a form of one
   field; these pages carry a list of dated rows and a form of ten, and at
   26rem the date and the status on a row stop fitting on one line. */
main.auth.portal {
    max-width: 60rem;
}

main.auth.portal .portal-who {
    margin-top: 1rem;
    color: var(--muted);
}

main.auth.portal .portal-actions {
    margin: 1.5rem 0;
}

/* The one action on the list, styled as a button because it is one — the
   anchor is only how a static page navigates. */
.button-link {
    display: inline-block;
    padding: 0.55rem 1.1rem;
    background: var(--green-deep);
    color: #ffffff;
    font-weight: 600;
    text-decoration: none;
}

.button-link:hover,
.button-link:focus {
    background: var(--green-dark);
}

/* Sign out is a form post, and a post is the only way a static page can do it
   safely — but it is not the thing on this screen anyone came to press, so it
   is drawn as the link it reads as. */
.link-button {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: var(--green-deep);
    text-decoration: underline;
    cursor: pointer;
}

main.auth.portal .portal-signout {
    margin-top: 2.5rem;
}

main.auth.portal .empty {
    padding: 1.25rem;
    border: 1px dashed var(--rule);
    color: var(--muted);
}

.request-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

/* The office's queue as large cards in two columns. Each card is a whole request — title, answer, notes
   and the controls to reply — so it gets a column of its own rather than a row to share. The columns
   fill one after the other, so a short card does not leave a gap under it while its neighbour is tall:
   the next card moves up under it. A narrow window falls back to one column. */
.request-list.request-cards {
    column-count: 2;
    column-gap: 1.25rem;
}

.request-cards > .request {
    break-inside: avoid;
    margin-bottom: 1.25rem;
    padding: 1.25rem 1.5rem;
}

.request-cards .request-head h2 {
    font-size: 1.25rem;
}

@media (max-width: 50rem) {
    .request-list.request-cards {
        column-count: 1;
    }
}

/* A rule down the side rather than a box, and the status decides its colour:
   the answer is the thing a person opens this page for, so it is legible from
   the edge of the row before any of the words are read. Colour is never the
   only carrier — .request-status spells it out alongside. */
.request {
    margin-bottom: 1rem;
    padding: 0.85rem 1rem;
    border: 1px solid var(--rule-light);
    border-left: 4px solid var(--rule);
    background: #ffffff;
}

.request.is-waiting {
    border-left-color: var(--warn);
}

/* Waiting on the requester, not the office: the same yellow as in review, but heavier, so the row that needs
   its owner's attention is the one they see first. */
.request.is-attention {
    border-left-color: #e0b400;
    border-left-width: 6px;
}

/* New: nobody has picked it up yet. Blue. In review: somebody has it in hand. Yellow. */
.request.is-new {
    border-left-color: #1f6fd1;
}

.request.is-review {
    border-left-color: #38bdf8;
}

.request.is-approved {
    border-left-color: var(--green);
}

/* An approved request by when it happens: magenta while it is on, charcoal and faded once it is over.
   Placed after is-approved so they win; the label says it in words too. */
.request.is-live {
    border-left-color: #d6249f;
    border-left-width: 6px;
}

.request.is-past {
    border-left-color: #1f2937;
    color: var(--muted);
}

.request.is-declined {
    border-left-color: var(--danger);
}

.request.is-withdrawn {
    border-left-color: var(--rule);
    color: var(--muted);
}

/* The room layout a requester attached, shown small on the card; the link opens it full size. */
.drawing-preview {
    display: block;
    margin-top: 0.4rem;
}

.drawing-preview img {
    display: block;
    max-width: 100%;
    max-height: 14rem;
    border: 1px solid var(--rule-light);
    background: #ffffff;
}

/* Title and status on one line, the status pushed to the end and allowed to
   drop below the title on a narrow screen rather than squeezing it. */
.request-head {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: baseline;
    justify-content: space-between;
}

.request-head h2 {
    margin: 0;
    font-size: 1.05rem;
}

.request-status {
    font-size: 0.85rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.is-waiting .request-status {
    color: var(--warn-ink);
}

/* Yellow, darkened just enough to read on white. */
.is-attention .request-status {
    color: #a68500;
}

.is-new .request-status {
    color: #174f99;
}

.is-review .request-status {
    color: #0369a1;
}

/* .request in front so these beat .is-approved .request-status, which is declared after them. */
.request.is-live .request-status {
    color: #b01a84;
}

.is-live .request-status::after {
    content: " · happening now";
}

.request.is-past .request-status {
    color: #1f2937;
}

.is-past .request-status::after {
    content: " · finished";
}

.is-approved .request-status {
    color: var(--green-deep);
}

.is-declined .request-status {
    color: var(--danger);
}

.is-withdrawn .request-status {
    color: var(--muted);
}

/* Tabular figures so a column of times lines up on the colon rather than
   wandering with the width of each digit. */
.request-when {
    margin: 0.35rem 0 0;
    font-variant-numeric: tabular-nums;
}

.request-where {
    margin: 0.15rem 0 0;
    color: var(--muted);
    font-size: 0.9rem;
}

/* What the office said. Set apart from the request's own words above it, so
   the answer is not read as part of the question. */
/* The requester's search, folded away until wanted. The summary is the whole click target, so it
   is set up to look like one. */
.request-search-panel {
    margin: 0 0 1rem;
    border: 1px solid var(--rule-light);
    background: #ffffff;
}

.request-search-panel > summary {
    padding: 0.6rem 0.9rem;
    cursor: pointer;
    font-weight: 600;
}

.request-search-panel > summary .aside {
    font-weight: 400;
}

.request-search-panel > .request-search {
    padding: 0 0.9rem 0.9rem;
}

.request-note {
    margin: 0.6rem 0 0;
    padding: 0.5rem 0.75rem;
    background: var(--green-tint);
    font-size: 0.9rem;
}

.is-declined .request-note {
    background: var(--danger-tint);
}

/* What is no longer available on a request that needs updating, on the requester's and the office's copy. */
.request-attention {
    margin: 0.6rem 0 0;
    padding: 0.5rem 0.75rem;
    background: var(--warn-tint, #fff4d6);
    font-size: 0.9rem;
}

/* The room an approved request was met with (FR-63), on both the requester's and the office's copy. */
.request-booked {
    margin: 0.4rem 0 0;
}

.decision h3 {
    margin-top: 0;
}

/* Equipment asked for with a request (FR-66): on the request form's second step and in the
   approver's panel. One row per piece, with a quantity beside anything pooled. */
.request-equipment {
    margin: 0.4rem 0 0;
}

.equipment-choices {
    list-style: none;
    margin: 0.75rem 0;
    padding: 0;
}

.equipment-choices li {
    padding: 0.5rem 0;
    border-top: 1px solid var(--rule);
}

/* The request form groups equipment into one box per type, boxes side by side where there is room. */
.equipment-groups {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: 0.75rem;
    margin: 0.75rem 0;
}

.equipment-group {
    margin: 0;
    padding: 0.25rem 0.75rem 0.5rem;
    border: 1px solid var(--rule);
    border-radius: 6px;
    min-width: 0;
}

.equipment-group legend {
    padding: 0 0.4rem;
    font-weight: 600;
}

.equipment-group .equipment-choices {
    margin: 0;
}

.equipment-group .equipment-choices li:first-child {
    border-top: 0;
}

/* In the approval form the list sits right under its heading and note, so the first item needs no rule above it. */
.decision .equipment-choices li:first-child {
    border-top: 0;
}

.equipment-choices label.checkbox {
    display: block;
    margin: 0;
    font-weight: 400;
}

.equipment-choices .aside {
    display: block;
    margin-left: 1.6rem;
    color: var(--muted);
    font-size: 0.85rem;
}

.equipment-choices label.quantity {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0.35rem 0 0 1.6rem;
    font-weight: 400;
}

main.auth .equipment-choices label.quantity input,
.equipment-choices label.quantity input {
    width: 6rem;
}

.equipment-choices label.quantity .aside {
    display: inline;
    margin: 0;
}

/* FR-67: other pending requests want this too. Amber rather than red: it is still on offer, and
   nothing is wrong yet. A help cursor says there is a tooltip to read. */
.pending-flag {
    margin-left: 0.3rem;
    color: var(--warn);
    cursor: help;
}

/* The approver's marks have no tooltip — the explanation is printed beside them — so no help cursor. */
.pending-flag.plain {
    cursor: default;
}

.request-summary {
    margin-top: 1rem;
    padding: 0.5rem 0.75rem;
    border-left: 3px solid var(--green);
}

.request-summary p {
    margin: 0.2rem 0;
}

.decision fieldset {
    border: none;
    padding: 0;
    margin: 1rem 0 0;
}

.decision legend {
    font-weight: 600;
}

/* --- The office's copy of a request (FR-34) -------------------------------
   The same rows the requester sees, plus the three things somebody answering
   one needs and the requester does not: who asked, what they said it was for,
   and what they would need set up. */
.request-who {
    margin: 0.35rem 0 0;
    font-size: 0.9rem;
}

.request-purpose {
    margin: 0.5rem 0 0;
}

.request-setup {
    margin: 0.35rem 0 0;
    font-size: 0.9rem;
}

/* The room-layout drawing sent with a request (FR-77): a link to open it. */
.request-drawing {
    margin: 0.4rem 0 0;
}

/* When it was sent, last and quietest: it settles which of two requests came
   first, which is a question asked far less often than any of the above. */
.request-filed {
    margin: 0.5rem 0 0;
    color: var(--muted);
    font-size: 0.8rem;
}

/* --- The request form -----------------------------------------------------
   Long enough that "which of these must I fill in" is a real question, so the
   optional ones say so on the label rather than leaving the required ones to
   be inferred from a marker on the others. */
main.auth.portal .optional {
    color: var(--muted);
    font-weight: 400;
}

main.auth.portal .field-hint {
    margin: 0.3rem 0 0;
    color: var(--muted);
    font-size: 0.85rem;
}

/* The layout drawing's drop box (request-form.js). */
main.auth.portal .drop-zone {
    padding: 0.75rem;
    border: 2px dashed var(--muted);
    border-radius: 6px;
}

main.auth.portal .drop-zone.dragging {
    border-style: solid;
    background: rgba(0, 0, 0, 0.06);
}

main.auth.portal .drop-zone-hint {
    margin: 0 0 0.4rem;
    color: var(--muted);
    font-size: 0.85rem;
}

main.auth.portal textarea,
main.auth.portal select {
    width: 100%;
    padding: 0.25rem 0.4rem;
    font: inherit;
    font-size: 0.9rem;
    box-sizing: border-box;
}

/* From and Until side by side: they are one answer, and stacked they read as
   two unrelated questions. They stack anyway below the width where two time
   inputs stop fitting. */
.field-pair {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
}

.field-pair > div {
    flex: 1 1 10rem;
}

/* The error summary carries a list; without this the bullets sit outside the
   tinted box the rule draws. */
main.auth .error ul {
    margin: 0;
    padding-left: 1.2rem;
}

/* --- Print ----------------------------------------------------------------
   A shell fixed to the height of the window prints one screenful and stops,
   because everything past it is scrolled out of a clipped box. Printing wants
   the document back: intrinsic height, nothing clipped. */
@media print {
    html,
    body {
        height: auto;
        display: block;
        overflow: visible;
    }

    .app-scroll {
        overflow: visible;
    }
}

/* Work list: the crew's and the technicians' day. */
.work-controls { display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-end; margin-block: 1rem; }
.work-booking { border-top: 1px solid var(--rule, #ccc); padding-block: 1rem; }
.work-tasks, .work-equipment, .work-contacts { list-style: none; padding: 0; margin: 0 0 1rem; }
.work-tasks li, .work-equipment li, .work-contacts li { padding-block: .25rem; }
.work-done { text-decoration: line-through; opacity: .7; }
.work-notes { background: var(--surface-alt, #f4f4f4); padding: .5rem .75rem; border-radius: .25rem; }

/* Booking page: the setup and teardown editor. */
.task-row { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .5rem; }
.task-row input[type="text"] { flex: 1 1 16rem; }
.tasks { padding-left: 1.25rem; }

/* --- Closure cards (hours and closures) ---------------------------------- */

/* The closures as a grid of cards beside one another, like the campus and room tiles, rather
   than one long column. Each card is the .gear card; the actions sit at the foot so a row of
   cards of different heights lines its buttons up. */
.results.closure-cards {
    grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
    margin: 1rem 0;
}

.closure-cards > li {
    display: flex;
    flex-direction: column;
    background: #fff;
}

.closure-cards .facts {
    margin: 0.5rem 0 0;
}

.closure-cards .actions {
    margin: auto 0 0;
    padding-top: 0.75rem;
}

/* --- Links drawn as buttons --------------------------------------------------
   A link that does something rather than goes somewhere in a sentence — "Edit this
   category", "Back to the catalog". The same outlined green as the secondary buttons
   beside it, so a row of the two reads as one set. */
main.page a.button-secondary {
    display: inline-block;
    padding: 0.4rem 0.9rem;
    font: inherit;
    background: #fff;
    border: 1px solid var(--green-dark);
    color: var(--green-deep);
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
}

main.page a.button-secondary:hover,
main.page a.button-secondary:focus-visible {
    background: var(--green-tint);
}

/* Date, From Time and To Time on one row — and Setup by's Date and Time above them, in the first two of
   the same three columns, so each input is exactly as wide as the event input beneath it. They stack
   below the width where three inputs stop fitting. */
.field-trio {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1rem;
}

@media (max-width: 36rem) {
    .field-trio {
        grid-template-columns: 1fr;
    }
}

