/*
    Frontier Schools Staff Evaluation

    Two things drive the visual design:
      * an evaluator is often on a tablet, standing up, in a classroom — hit areas
        are generous and nothing important depends on hover (spec section 96);
      * this is a personnel record, so anything that changes what an employee will
        eventually see is labelled unmistakably (spec section 26).
*/

/*
    Geist Sans, served from this application rather than from Google.

    Self-hosted for three reasons: the content security policy allows no external
    origins, a school network that blocks fonts.gstatic.com would otherwise fall
    back silently, and a personnel system should not tell a third party which pages
    a district's staff are opening.

    One variable file covers 100-900, so every weight on the site is a single
    download rather than four.

    Bricolage Grotesque is still in wwwroot/lib if the district wants it back: the
    swap is this block and the --fs-font token below, nothing else.
*/
@font-face {
    font-family: "Geist Sans";
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url("../lib/geist/geist-variable.woff2") format("woff2");
}

:root {
    /*
        The palette — Frontier Schools navy and gold.

        Two colours carry the brand and they are not interchangeable. Navy is the
        primary: chrome, primary buttons, active states, headings, the first series
        on every chart. Gold is an accent and nothing else — an active indicator, a
        progress marker, a badge, a key data point. Large areas of gold are what
        turns a district's palette into a hazard sign, so there are none.

        The two must also be used together rather than apart. Gold on white is
        1.9:1 and unreadable at any size, so gold text on a light ground does not
        exist here — see --fs-gold-ink for the times something gold-flavoured has
        to be read. Gold on navy is 9.7:1, which is why the accent lives on the
        navy chrome.
    */
    --fs-navy: #001842;
    --fs-navy-soft: #0b2a5c;
    --fs-navy-deep: #000f2b;

    --fs-gold: #ffb504;
    --fs-gold-dark: #d99700;
    --fs-gold-soft: #fff4d9;
    /* Gold's own hue, dark enough to read as text on white (6.4:1). The accent
       itself never is. */
    --fs-gold-ink: #7a5200;

    /*
        Primary.

        Repointed from a mid blue to the brand navy, which reaches every primary
        button, active border and selected state in one move. Its "dark" partner is
        the pressed/hover step — deeper still, because a navy button cannot get
        darker by much and a lighter hover reads as the button losing focus.
    */
    --fs-blue: #001842;
    --fs-blue-dark: #000f2b;
    --fs-blue-soft: #e9edf5;

    /*
        Links.

        Navy cannot be the link colour: at #001842 against body text at #1b2333 the
        two are indistinguishable, and a page of links that look like prose is worse
        than an off-brand blue. This is the brand hue lifted until it separates —
        8.3:1 on white, and unmistakably the same family as the chrome.
    */
    /* --fs-link/-hover removed: links are --fs-navy with an underline, per the
       portal. A token nothing uses is a decision nobody can find. */

    --fs-ink: #001233;

    /*
        Body text.

        Was a mid slate, which read as grey against white and left every page looking
        faded. Near-black with a trace of blue instead: it sits with the navy chrome
        without going flat black, and clears 15:1 against a white card, which is what
        a tablet held at arm's length in a lit classroom needs.
    */
    --fs-body: #14203a;

    /* --pd-muted. 5.6:1 on white, which is past AA for body text. */
    --fs-muted: #5c6880;
    /*
        Borders.

        The canvas is #fffcff and the cards on it are white, so the gap between the
        two is about two percent of luminance — a border tinted anywhere near either
        of them does not read as an edge at all, it reads as a rendering artefact.
        These three are set against the canvas rather than against white, which is
        why they look heavier in isolation than a swatch chart would suggest.

        Three weights, and the distinction is what each one has to survive:

          --fs-line-soft   inside a card, between rows of the same thing. Only has
                           to separate, not to enclose.
          --fs-line        the edge of a card, a table, a panel. Has to hold against
                           the canvas behind it.
          --fs-line-strong anything a person is about to type into or click, read at
                           arm's length on a tablet in a bright classroom.
    */
    /*
        Taken from the PD portal's --pd-line / --pd-border / --pd-border-strong
        rather than tuned here.

        These were warm, on the reasoning that a warm canvas wants warm borders.
        The sibling application runs the same canvas with navy-tinted lines and it
        is the documented system, so two Frontier applications open side by side
        now draw an edge the same way. Consistency between them beats a local
        preference.
    */
    --fs-line: #cfd4de;
    --fs-line-strong: #9aa3b5;
    --fs-line-soft: #e6e3e6;

    /*
        The canvas is now the brand's off-white, and it is all but the same colour
        as a white card — under half a per cent of luminance between them. The old
        canvas was a cool grey and did half the work of separating the two, so the
        border above had only to finish the job.

        It cannot any more. Every card edge on this application is now carried by
        --fs-line alone, which is why that border is warm and a full step darker
        than the one it replaces: on this ground a fainter line is not a subtle
        edge, it is no edge.
    */
    --fs-surface: #ffffff;
    --fs-canvas: #fffcff;

    /* The brand's neutral. Secondary cards, inactive controls, filter bars, empty
       states — anything that should recede without becoming a hole in the page. */
    --fs-neutral: #ece7e5;
    --fs-neutral-dark: #ddd6d3;
    --fs-neutral-soft: #f5f2f1;

    --fs-green: #0f7a4a;
    --fs-green-soft: #e6f6ee;
    --fs-amber: #a5610b;
    --fs-amber-soft: #fdf2e0;
    --fs-red: #bb2b26;
    --fs-red-soft: #fdecec;
    --fs-violet: #6435cf;
    --fs-violet-soft: #f0ebfd;
    --fs-teal: #0f7490;
    --fs-teal-soft: #e4f4f8;

    /*
        Layout scale.

        Everything that decides how wide the application feels is here, expressed
        against the viewport rather than as fixed pixel steps — so a 1280 laptop, a
        1920 monitor and a 2560 display each get a sensible amount of room without a
        breakpoint for every size.

            1280 → ~1210 content    1920 → ~1810    2560 → 2200 (capped)

        The cap is deliberate. Past roughly 2200px a table stops being easier to read
        and starts being something you scan by turning your head.
    */
    --fs-shell: min(2200px, 95vw);
    --fs-gutter: clamp(.75rem, 1.6vw, 2.25rem);
    --fs-form-max: clamp(640px, 58vw, 1120px);

    /*
        Four steps, and the distinction is the size of the thing being rounded.
        A 16px radius on a checkbox looks like a mistake; an 8px radius on a
        dashboard panel looks like nothing at all.
    */
    --fs-radius-sm: 8px;       /* small controls, pills' square cousins */
    --fs-radius-control: 11px; /* inputs, buttons, selects (--pd-r-md) */
    --fs-radius: 16px;         /* cards */
    --fs-radius-lg: 20px;      /* large dashboard panels */

    /* Two soft layers rather than one hard one: a card should look lifted off the
       canvas, not stamped onto it. */
    /* Tinted to the brand navy rather than a neutral black, and lighter than
       before: on an off-white canvas the shadow is doing less work than the
       border, and a heavy one under every card reads as clutter. */
    --fs-shadow: 0 1px 2px rgba(0, 24, 66, .04), 0 5px 14px -6px rgba(0, 24, 66, .07);
    --fs-shadow-lg: 0 12px 40px -8px rgba(0, 24, 66, .16);

    --fs-font: "Geist Sans", "Segoe UI", system-ui, -apple-system,
        "Helvetica Neue", Arial, sans-serif;
}

/*
    Bootstrap's own variables, pointed at the palette above.

    Large parts of this application are built from Bootstrap components and from
    `--bs-*` tokens directly — the observation workspace and the administrator
    protocol both are. Redefining the tokens once here means those screens follow
    the palette without every one of them needing to be rewritten, and means a
    future change to the palette reaches all of it.
*/
:root {
    --bs-body-font-family: var(--fs-font);
    --bs-font-sans-serif: var(--fs-font);
    --bs-body-color: var(--fs-body);
    --bs-body-bg: var(--fs-surface);
    --bs-secondary-color: var(--fs-muted);
    --bs-secondary-bg: var(--fs-line-soft);
    --bs-tertiary-bg: var(--fs-canvas);
    --bs-border-color: var(--fs-line);
    --bs-border-radius: var(--fs-radius-control);
    --bs-emphasis-color: var(--fs-ink);

    --bs-primary: var(--fs-navy);
    --bs-primary-rgb: 0, 24, 66;
    --bs-primary-bg-subtle: var(--fs-blue-soft);
    --bs-primary-border-subtle: #c2ccdd;
    --bs-primary-text-emphasis: var(--fs-navy);

    --bs-success: var(--fs-green);
    --bs-success-bg-subtle: var(--fs-green-soft);
    --bs-success-border-subtle: #b7e2cb;
    --bs-success-text-emphasis: #0b5c38;

    --bs-warning: var(--fs-amber);
    --bs-warning-bg-subtle: var(--fs-amber-soft);
    --bs-warning-border-subtle: #f2d3a0;
    --bs-warning-text-emphasis: #834c08;

    --bs-danger: var(--fs-red);
    --bs-danger-bg-subtle: var(--fs-red-soft);
    --bs-danger-border-subtle: #f3c2c0;
    --bs-danger-text-emphasis: #93211d;

    --bs-info: var(--fs-teal);
    --bs-info-bg-subtle: var(--fs-teal-soft);
    --bs-info-border-subtle: #b3ddea;
    --bs-info-text-emphasis: #0b596f;
}

* { box-sizing: border-box; }

body {
    margin: 0;

    /*
        The shell fills the window even when the content does not. Without this the
        footer sat wherever the content happened to end — halfway up a tall window
        on a short list, canvas showing below it, which reads as a page that failed
        to finish loading rather than one that is simply short.

        The full-bleed rule further down overrides the height for the workspace and
        the form builder, which fill the window exactly rather than at least.
    */
    min-height: 100vh;
    display: flex;
    flex-direction: column;

    background: var(--fs-canvas);
    color: var(--fs-body);
    font-family: var(--fs-font);
    font-size: 15px;
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;

    /*
        Geist has no optical-size axis, so this does nothing on it — kept because it
        costs nothing and is correct the moment a face that does have one is set.
    */
    font-optical-sizing: auto;
}

/* Controls inherit nothing by default, and a form full of Segoe next to a page of
   Bricolage is the single most obvious way a font swap looks half-finished. */
input, select, textarea, button, .form-control, .form-select, .btn {
    font-family: inherit;
}

h1, h2, h3, h4, h5 {
    color: var(--fs-ink);
    font-weight: 650;
    letter-spacing: -.018em;
    line-height: 1.25;
}

h1 { font-size: 1.7rem; }
h2 { font-size: 1.3rem; }
h3 { font-size: 1.05rem; }

/* Numbers in tables, scores and counters line up under each other. Geist has
   tabular figures; without asking for them a column of 7s and 1s wanders. */
table, .fs-stat__value, .fs-num { font-variant-numeric: tabular-nums; }

/*
    Navy from the brand, and no underline at rest.

    The underline was borrowed from the portal, where links in prose carry one.
    It does not survive contact with this application: whole components here are
    anchors — every settings card, every stat tile, every nav tab — so a blanket
    underline ruled through card titles and their descriptions, three lines at a
    time. A rule that has to be switched off by two dozen components is the wrong
    default.

    So the underline is a hover affordance instead. Nothing is ruled through until
    a pointer is on it, and the components that were never meant to look like
    links no longer do.
*/
a { color: var(--fs-navy); text-decoration: none; }

a:hover {
    color: var(--fs-navy-soft);
    text-decoration: underline;
    text-decoration-color: rgba(0, 24, 66, .3);
}

/*
    Components that happen to be anchors, which is most of the ones on these
    pages: a settings card, a stat tile, a nav tab, the app-switcher tiles. They
    are surfaces you click, not words in a sentence, and ruling a three-line card
    description through on hover reads as damage rather than as an affordance —
    each of them already answers the hover with a border, a fill or a lift.

    Taken from the views rather than guessed: this is every class that appears on
    an <a> in the templates.
*/
a.fs-setting:hover,
a.fs-settings__start:hover,
a.fs-stat:hover,
a.qc-tile:hover,
a.fs-tabs__tab:hover,
a.fs-iconbtn:hover,
a.fs-dd__head:hover,
a.lp-rail__step:hover,
a.app-brand:hover,
a.app-bell:hover,
.app-nav a:hover,
.app-more__panel a:hover { text-decoration: none; }

/* ------------------------------------------------------------------ Header */

/* =================================================== The application shell */

/*
    A column of sections on the left, everything else to the right of it.

    The sections were a strip across the top, and that is what forced the overflow
    menu: a dozen of them will not sit on one row, so the strip scrolled sideways
    on a laptop and stood three rows tall on a tablet — which moved every page
    down the screen as the window changed. A column holds all of them at any
    width and never reorders, so a section stays where somebody learnt it was.
*/
:root { --fs-sidebar-w: 222px; }

body.app-shell {
    display: flex;
    align-items: stretch;
    min-height: 100vh;
}

.app-sidebar {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 1035;

    width: var(--fs-sidebar-w);
    flex: none;
    display: flex;
    flex-direction: column;

    background: var(--fs-navy);
    overflow-y: auto;
    overscroll-behavior: contain;
}

.app-body {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

/*
    Inset by the column's width, and only where there is a column.

    This margin sat on .app-body unconditionally at first, which pushed the
    sign-in page 250px to the right — that page has no sidebar, because nobody is
    signed in to have sections yet. Scoped to .app-shell, which the layout puts on
    <body> only for an authenticated page.

    Fixed rather than in the flow, so the column does not scroll away from under a
    long table.
*/
body.app-shell .app-body { margin-left: var(--fs-sidebar-w); }

/*
    The page as a surface tucked into the navy shell.

    Everything below the top bar is one light panel with its top-left corner
    rounded, so the sidebar and the bar read as a frame the content sits inside
    rather than three flat bands stacked against each other. Only that one corner:
    the other three meet the window edge, and rounding a corner with nothing behind
    it just cuts the content off.

    The navy behind it comes from .app-body, which is what shows through the curve.
*/
body.app-shell .app-body { background: var(--fs-navy); }

body.app-shell .app-main {
    background: var(--fs-canvas);
    border-top-left-radius: var(--fs-radius-lg);
}

/* The footer is part of the same surface, so the panel does not stop short of the
   bottom of the window and leave a navy band under it. */
body.app-shell .app-footer { border-top-left-radius: 0; }

/* ------------------------------------------------------------- The brand */

.app-brand {
    display: flex;
    align-items: center;
    gap: .7rem;
    padding: 1.1rem .9rem;
    flex: none;
}

.app-brand:hover { text-decoration: none; }

/*
    The F mark on a white tile.

    Its F is black, so it cannot sit straight on the navy — and recolouring
    somebody's logo is the one thing the brand rules forbid outright. A tile is the
    standard answer, and the supplied lockup uses one.

    Square, because the mark is a letterform rather than a wordmark: a tile shaped
    to the old horizontal logo left it floating in a band of white.
*/
.app-brand__plate {
    display: grid;
    place-items: center;
    width: 54px;
    height: 54px;
    flex: none;
    background: #fff;
    border-radius: 14px;
}

/* Capped by height with width auto, so the aspect ratio is never touched. */
.app-brand__logo {
    display: block;
    max-height: 34px;
    max-width: 34px;
    width: auto;
    height: auto;
}

/*
    The name, set rather than drawn.

    The two words are different colours in the supplied lockup and a single image
    cannot do that against this ground. Set as text they also stay crisp at any
    zoom, and the anchor carries one accessible name for both.
*/
.app-brand__word {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.05;
}

.app-brand__word-a {
    font-size: 1.15rem;
    font-weight: 800;
    letter-spacing: .01em;
    color: #fff;
}

/*
    Tracked out until it sits under FRONTIER at about the same width, which is what
    the lockup does. The trailing letter's space is trimmed back, or the word reads
    as though it is shifted left of the one above it.
*/
/*
    Spread to exactly FRONTIER's width, flush at both ends.

    The column is stretch-aligned, so this box is already as wide as the longer
    word above it; space-between then puts the first letter hard left, the last
    hard right and the slack evenly between. The alignment holds at any font size,
    which a fixed letter-spacing never did — that added a gap after the final
    letter too, so the word sat short of its own right edge by one space.
*/
.app-brand__word-b {
    display: flex;
    justify-content: space-between;
    font-size: .82rem;
    font-weight: 700;
    color: var(--fs-gold);
}

/* The logo whole, on the one screen with a light background behind it. */
.fs-signin__logo {
    display: block;
    width: 100%;
    max-width: 220px;
    height: auto;
    margin-inline: auto;
}

/* ---------------------------------------------------------- The sections */

.app-sidenav {
    display: flex;
    flex-direction: column;
    gap: .1rem;
    padding: .35rem .6rem 1.5rem;
}

.app-sidenav a {
    display: flex;
    align-items: center;
    gap: .6rem;

    /* The 3px rail on the active item is drawn inside this padding, so every row
       keeps the same text position whether or not it is the current one. */
    padding: .58rem .75rem;
    border-radius: var(--fs-radius-sm);

    color: #c6d5e8;
    font-size: .875rem;
    font-weight: 500;
    line-height: 1.3;
}

.app-sidenav a:hover {
    background: rgba(255, 255, 255, .09);
    color: #fff;
    text-decoration: none;
}

.app-sidenav a i { opacity: .8; flex: none; width: 1.1rem; text-align: center; }

.app-sidenav a:focus-visible { outline: 2px solid #fff; outline-offset: -2px; }

/*
    Where you are.

    Gold earns its place here: on navy it is the one accent that says "this one"
    without a second colour of chrome, and the pairing is 9.8:1. The rail is a
    left edge now rather than an underline, which is what a vertical list wants —
    and the weight and background lift stay, because colour must never be the only
    thing carrying a state.
*/
.app-sidenav a.is-here {
    background: rgba(255, 255, 255, .13);
    color: #fff;
    font-weight: 600;
    box-shadow: inset 3px 0 0 var(--fs-gold);
}

.app-sidenav a.is-here i { opacity: 1; color: var(--fs-gold); }

/* ------------------------------------------------------------ The top bar */

.app-topbar {
    position: sticky;
    top: 0;
    z-index: 1030;

    display: flex;
    align-items: center;
    gap: 1rem;
    min-height: 60px;

    /* Tighter on the left than the page below it. The product name is a label on
       the shell rather than the start of the content, so it sits close to the
       column it belongs to instead of lining up with the page title. */
    padding: .5rem var(--fs-gutter) .5rem 1.1rem;

    background: var(--fs-navy);
    color: #e8eef7;

    /* No drop shadow. The page below is now a rounded surface sitting on the navy,
       and a shadow cast across that corner muddied the curve it is meant to show. */
    box-shadow: none;
}

/*
    What this application is.

    Uppercase and tracked rather than set as a heading: it names the product, and
    at sentence case beside a page title it read as a second title competing with
    the real one.
*/
.app-topbar__product {
    font-size: 1.22rem;
    font-weight: 700;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: #fff;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.app-user { display: flex; align-items: center; gap: .6rem; margin-left: auto; }

/* ------------------------------------------------- Opening the column ---- */

/*
    Hidden while the column is on screen. A control that opens something already
    visible is a control that does nothing.
*/
.app-burger {
    display: none;
    place-items: center;
    width: 40px;
    height: 40px;
    flex: none;
    border: none;
    border-radius: var(--fs-radius-control);
    background: none;
    color: #c6d5e8;
    cursor: pointer;
}

.app-burger:hover { background: rgba(255, 255, 255, .14); color: #fff; }
.app-burger:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

.app-burger__bars,
.app-burger__bars::before,
.app-burger__bars::after {
    display: block;
    width: 1.15rem;
    height: 2.5px;
    border-radius: 2px;
    background: currentColor;
}

.app-burger__bars { position: relative; }

.app-burger__bars::before,
.app-burger__bars::after { content: ""; position: absolute; left: 0; }
.app-burger__bars::before { top: -6px; }
.app-burger__bars::after { top: 6px; }

.app-scrim { display: none; }

/*
    Below 1200px the column comes off the layout and slides in over the page.

    The breakpoint is where 250px of chrome stops being affordable rather than a
    device size: a 1024px tablet gives the content 774px, which is narrower than
    the staff table wants.
*/
@media (max-width: 1200px) {
    .app-sidebar {
        transform: translateX(-100%);
        transition: transform .18s ease;
        box-shadow: 0 0 40px rgba(0, 24, 66, .45);
    }

    body.app-sidebar-open .app-sidebar { transform: translateX(0); }

    /* Same specificity as the rule that sets it, or it does not win: scoping
       that margin to body.app-shell outranked a bare .app-body here and left a
       250px empty gutter beside a sidebar that had slid off screen. */
    body.app-shell .app-body { margin-left: 0; }

    .app-burger { display: grid; }

    body.app-sidebar-open .app-scrim {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 1034;
        border: none;
        padding: 0;
        background: rgba(0, 24, 66, .45);
    }
}

/* The slide is decoration; the column arrives either way. */
@media (prefers-reduced-motion: reduce) {
    .app-sidebar { transition: none; }
}



/* -------------------------------------------------------------------- Main */

/*
    The page shell.

    Bootstrap's .container-xxl stopped at 1320px, which left roughly a third of a
    1920 screen empty and forced the staff table to scroll sideways for no reason.
    This scales instead: nearly the full width on a laptop, wider on a large monitor,
    and capped well beyond that so a very large display does not turn a table into
    something you have to move your head to read.
*/
.app-main {
    width: 100%;

    /* Takes the slack in the column above, so the footer is pushed to the bottom of
       a short page rather than floating in the middle of it. */
    flex: 1 0 auto;

    max-width: var(--fs-shell);
    margin: 0 auto;
    padding: clamp(1rem, 1.6vw, 2rem) var(--fs-gutter) 3rem;
}

/*
    A full-bleed page fills the window height. It does not take the whole of its
    width as well.

    Live notes used to run to the glass on both sides while the evaluation form — the
    other half of the same visit, one tab away — sat in the measured column every
    other screen uses. Switching between the two moved the page under the reader.

    Height is what full bleed is for here: the evidence stream and the framework panel
    scroll inside a page that never does. Width is not, so it comes back into line.
*/
.app-main--full {
    padding: 0 var(--fs-gutter) var(--fs-gutter);
    max-width: var(--fs-shell);
    margin: 0 auto;
}
.app-fullbleed { overflow: hidden; }

/*
    A full-bleed page fills the window exactly: the header takes what it needs and
    the page takes the rest.

    The panels inside used to size themselves with calc(100vh - 56px), which assumes
    a 56px header. It is 56px only while the navigation fits on one row — it needs
    about 1700px to do that, stands 80px tall on a laptop and stood 242px tall on an
    iPad, so the page ran off the bottom of a window that cannot scroll back. A flex
    column measures rather than assuming, and holds whatever the header wraps to.
*/
body.app-fullbleed { height: 100vh; overflow: hidden; }

/* The body column is what fills the window now — the sidebar beside it is fixed
   and full height already, so the old rule (a flex column on <body>) would have
   stacked the column on top of the page instead of beside it. */
body.app-fullbleed .app-body { height: 100vh; min-height: 0; }

body.app-fullbleed .app-main--full {
    display: flex;
    flex-direction: column;
    flex: 1;

    /*
        As wide as the bar above it.

        The shell cap centres the column and leaves a margin either side, which is
        right for a page of prose and wrong here: the bar is full width, so the white
        stopped short of it at both ends and the two read as unrelated bands. A
        workspace is a tool, not a document — it can have the whole window.
    */
    max-width: none;

    /* Without this a flex child refuses to shrink below its content, which puts the
       overflow straight back. */
    min-height: 0;
}

/* The panels stop measuring the window for themselves. */
/*
    A descendant, not a child.

    The main element is inside .app-body — the column that sits beside the fixed
    sidebar — so "body > .app-main--full" matched nothing and every panel below kept
    its calc(100vh - 56px) after all. It fit anyway while the header really was 56px
    tall and nothing else was above it, which is why this went unnoticed: the
    impersonation banner is the first thing to add height above the workspace, and
    the page then ran exactly one banner past the bottom of a window that cannot
    scroll. Measuring instead of assuming holds for the banner, for a wrapped header,
    and for whatever is put up there next.
*/
body.app-fullbleed .app-main--full > .ow,
body.app-fullbleed .app-main--full > .fb {
    height: auto;
    flex: 1;
    min-height: 0;
}

/*
    Except on a phone, where the window is not big enough to hold the whole thing at
    any division of it.

    Locked to the viewport, the workspace gave its evidence stream 24 pixels of an
    844px screen — the notes already taken were, in practice, not on the page, and
    what was left was three scroll regions nested inside 350px. So below this width
    the page stops filling the window and grows to its content, and the whole thing
    scrolls once, the way a phone page normally does.
*/
@media (max-width: 575.98px) {
    body.app-fullbleed {
        height: auto;
        min-height: 100vh;
        overflow: visible;
    }

    body.app-fullbleed .app-main--full,
    body.app-fullbleed .app-main--full > .ow,
    body.app-fullbleed .app-main--full > .fb {
        min-height: 0;
        flex: 0 0 auto;
    }
}

/*
    Reading width for a single-column form.

    Wider than it was, because a form squeezed to 720px on a 1920 screen looks
    broken — but still bounded, because a text field the width of a monitor is
    genuinely harder to fill in.
*/
.fs-form { width: 100%; max-width: var(--fs-form-max); }
.fs-form--narrow { width: 100%; max-width: clamp(480px, 42vw, 720px); }

.app-footer {
    border-top: 1px solid var(--fs-line);
    background: #fff;
    color: var(--fs-muted);
    font-size: .78rem;
    padding: .85rem 0;
}

/* ------------------------------------------------------------------- Cards */

/*
    A card.

    Padded by default. It was not, originally — the padding lived on .fs-card__body
    and every card was expected to carry one. Cards written without that wrapper had
    their text against the border, which is a rule nobody can be expected to remember
    for every new screen. So the card pads itself, and stands aside where a head or a
    body is present to do it.
*/
.fs-card {
    padding: 1.1rem;
    background: var(--fs-surface);
    border: 1px solid var(--fs-line);
    border-radius: var(--fs-radius);
    box-shadow: var(--fs-shadow);
}

.fs-card:has(> .fs-card__body),
.fs-card:has(> .fs-card__head) { padding: 0; }

/*
    A card whose whole content is a table says so with p-0 — Bootstrap's, which
    carries !important and wins. Detecting a table child instead was too broad: it
    also unpadded cards that hold a filter, some totals and then a table, which is
    most of them.

    Inside a padded card the table keeps its own cell padding, so it is inset from the
    card edge by a little more than the text above it. That reads as the table being a
    thing within the section, which it is.
*/
.fs-card > .table-responsive > .fs-table { margin-bottom: 0; }

.fs-card__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: .85rem 1.1rem;
    border-bottom: 1px solid var(--fs-line-soft);
}

.fs-card__head h2, .fs-card__head h3 {
    margin: 0;
    font-size: .95rem;
    font-weight: 650;
    letter-spacing: -.01em;
}
.fs-card__body { padding: 1.1rem; }
.fs-card__body--flush { padding: 0; }

/* ------------------------------------------------------------- Stat blocks */

.fs-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: .85rem;
}

.fs-stat {
    background: var(--fs-surface);
    border: 1px solid var(--fs-line);
    border-radius: var(--fs-radius);
    padding: .95rem 1.05rem;
    box-shadow: var(--fs-shadow);
}

.fs-stat__value { font-size: 1.75rem; font-weight: 650; color: var(--fs-ink); line-height: 1.1; }
.fs-stat__label { font-size: .78rem; color: var(--fs-muted); text-transform: uppercase; letter-spacing: .04em; }
.fs-stat--alert .fs-stat__value { color: var(--fs-red); }
.fs-stat--warn .fs-stat__value { color: var(--fs-amber); }

/* "3 of 8" — the target beside the count, not under it. The number is what is
   read; the denominator only gives it meaning, so it stays at label weight. */
/* ------------------------------------------------- The plan, grouped and collapsible

   One <details> per heading of the path. The chevron turns rather than swapping glyph,
   so the row does not reflow by a pixel when it opens. */
.fs-plan { border-bottom: 1px solid var(--fs-line-soft); }
.fs-plan:last-child { border-bottom: 0; }

.fs-plan__head {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .7rem 1rem;
    cursor: pointer;
    list-style: none;
    user-select: none;
}

.fs-plan__head::-webkit-details-marker { display: none; }
.fs-plan__head:hover { background: var(--fs-line-soft); }

.fs-plan__chevron {
    color: var(--fs-muted);
    font-size: .8rem;
    transition: transform .12s ease;
}

.fs-plan[open] .fs-plan__chevron { transform: rotate(90deg); }

.fs-plan__name { font-weight: 600; flex: 1 1 auto; }

.fs-plan__count {
    font-size: .8rem;
    color: var(--fs-muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.fs-plan__count.is-done { color: var(--fs-green); }

.fs-plan__steps {
    list-style: none;
    margin: 0;
    padding: 0 0 .5rem 0;
}

.fs-plan__step {
    display: flex;
    align-items: baseline;
    gap: .6rem;
    padding: .35rem 1rem .35rem 2.4rem;
    font-size: .9rem;
}

.fs-plan__step-name { flex: 1 1 auto; }
.fs-plan__tick { color: var(--fs-green); }
.fs-plan__todo { color: var(--fs-line-strong); }

.fs-plan__when {
    font-size: .8rem;
    color: var(--fs-muted);
    white-space: nowrap;
}

/* The menu grid: one group's sections, as many across as the card allows. A table
   with a column per section forces a horizontal scroll on a laptop for a list that
   is really just checkboxes. */
/* A switch, for a setting that is on or off rather than one of a list. Built from a
   real checkbox so it is reachable by keyboard and reports its own state; the box
   itself is moved off-screen rather than hidden, because display:none takes it out
   of the tab order and out of the accessibility tree with it. */
.fs-switch { display: flex; align-items: center; gap: .6rem; }

.fs-switch__input {
    position: absolute;
    opacity: 0;
    width: 2.4rem;
    height: 1.3rem;
    margin: 0;
    cursor: pointer;
}

.fs-switch__label {
    display: flex;
    align-items: center;
    gap: .6rem;
    cursor: pointer;
    font-weight: 500;
}

.fs-switch__label::before {
    content: "";
    flex: none;
    width: 2.4rem;
    height: 1.3rem;
    border-radius: 1rem;
    background: var(--fs-line-strong);
    transition: background .12s ease;
}

.fs-switch__label::after {
    content: "";
    position: absolute;
    width: 1rem;
    height: 1rem;
    margin-left: .15rem;
    border-radius: 50%;
    background: #fff;
    transition: transform .12s ease;
}

.fs-switch { position: relative; }

.fs-switch__input:checked + .fs-switch__label::before { background: var(--fs-blue); }
.fs-switch__input:checked + .fs-switch__label::after { transform: translateX(1.1rem); }

/* The focus ring belongs on the thing that looks like the control. */
.fs-switch__input:focus-visible + .fs-switch__label::before {
    outline: 2px solid var(--fs-blue);
    outline-offset: 2px;
}

/*
    A smaller switch, for a place where two of them sit inside a table row rather
    than on a settings form of their own. Everything scales from the track, and
    the knob's travel is the track width less the knob and its two margins.
*/
.fs-switch--sm .fs-switch__label { gap: .45rem; font-size: .78rem; }

.fs-switch--sm .fs-switch__input,
.fs-switch--sm .fs-switch__label::before { width: 1.9rem; height: 1.05rem; }

.fs-switch--sm .fs-switch__label::after { width: .8rem; height: .8rem; }

.fs-switch--sm .fs-switch__input:checked + .fs-switch__label::after {
    transform: translateX(.83rem);
}

/* "A password is saved" — a fact worth reading as reassurance rather than as a hint. */
.fs-saved { color: var(--fs-green); }

/* A field with a reveal button welded to its end. The button is a peer of the input
   rather than sitting inside it, so it is reachable by keyboard in the order a
   person expects and does not have to be positioned over the text. */
.fs-reveal { display: flex; }

.fs-reveal > .form-control {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
}

.fs-reveal > .btn {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
    margin-left: -1px;
}

/* One component of the framework, as the person it is about reads it. Bordered so
   an empty one still looks like a thing that was asked about rather than a gap. */
.fs-comp { display: grid; gap: .6rem; }

.fs-comp__item {
    border: 1px solid var(--fs-line-soft);
    border-left: 3px solid var(--fs-line);
    border-radius: var(--fs-radius-sm);
    padding: .6rem .8rem;
}

/* Seen, and therefore the row a reader is looking for. */
.fs-comp__item.is-observed { border-left-color: var(--fs-green); }

.fs-comp__head {
    display: flex;
    align-items: center;
    gap: .4rem;
    flex-wrap: wrap;
    margin-bottom: .4rem;
}

.fs-comp__title { font-weight: 600; font-size: .9rem; }

/* Rows sit under their heading, not level with it. Indented on the name column
   only: indenting the whole row would drag the dates out of line with the ones in
   the heading above, which is what makes a column of dates readable. */
.fs-folder__name { padding-left: 2.1rem !important; }

/* A heading of the path, in a folder. Banded rather than ruled: an extra line in a
   table of dates reads as part of the grid, a band reads as a break in it. */
.fs-folder__headingrow th {
    background: var(--fs-canvas);
    font-size: .85rem;
    padding-top: .55rem;
    padding-bottom: .55rem;
    border-top: 2px solid var(--fs-line);
}

.fs-folder__heading:first-child .fs-folder__headingrow th { border-top: 0; }

/* A building and its administrators, in the goal table. The heading row is a band
   rather than a heavier border: with fifteen numeric columns an extra rule reads as
   part of the grid, and a band reads as a break in it. */
.rp-school__head th {
    background: var(--fs-canvas);
    font-size: .85rem;
    letter-spacing: .02em;
    padding-top: .6rem;
    padding-bottom: .6rem;
    border-top: 2px solid var(--fs-line);
}

.rp-school__total > td { background: var(--fs-line-soft); font-weight: 600; }

.fs-menugrid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
    gap: .5rem 1rem;
}

.fs-menugrid__item {
    display: flex;
    align-items: baseline;
    gap: .5rem;
}

/* Tickable, but it will not do anything until the permission is granted. Dimmed
   rather than disabled: a district is often about to grant it. */
.fs-menugrid__item.is-blocked .form-check-label { color: var(--fs-muted); }

.fs-stat__of {
    font-size: .8rem;
    font-weight: 500;
    color: var(--fs-muted);
    margin-left: .35rem;
}

/* ------------------------------------------------------------------- Pills */

.fs-pill {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    padding: .16rem .5rem;
    border-radius: 999px;
    font-size: .74rem;
    font-weight: 600;
    line-height: 1.4;
    white-space: nowrap;
}

.fs-pill--component { background: #e9edf5; color: var(--fs-blue-dark); }
.fs-pill--lookfor { background: var(--fs-violet-soft); color: var(--fs-violet); }
.fs-pill--strong { background: var(--fs-green-soft); color: var(--fs-green); }
.fs-pill--possible { background: var(--fs-amber-soft); color: var(--fs-amber); }
.fs-pill--weak { background: var(--fs-line-soft); color: var(--fs-muted); }
.fs-pill--private { background: #efebea; color: #3d4553; }
.fs-pill--visible { background: var(--fs-green-soft); color: var(--fs-green); }
.fs-pill--admin { background: var(--fs-red-soft); color: var(--fs-red); }
.fs-pill--draft { background: var(--fs-line-soft); color: var(--fs-muted); }
.fs-pill--published { background: var(--fs-green-soft); color: var(--fs-green); }
.fs-pill--inuse { background: #e9edf5; color: var(--fs-blue-dark); }
.fs-pill--locked { background: var(--fs-amber-soft); color: var(--fs-amber); }
.fs-pill--archived { background: #efebea; color: #5a6270; }

/* --------------------------------------------------------- Empty and hints */

/*
    An empty state on the brand's off-white canvas needs a ground of its own.
    White-on-white said nothing at all: the region reads as a page that failed to
    render rather than as a table with nothing in it yet.
*/
.fs-empty {
    text-align: center;
    padding: 2.5rem 1rem;
    color: var(--fs-muted);
    background: var(--fs-neutral-soft);
    border-radius: var(--fs-radius-sm);
}

.fs-empty i { font-size: 1.8rem; opacity: .45; display: block; margin-bottom: .5rem; }

.fs-hint { font-size: .82rem; color: var(--fs-muted); }

.fs-note {
    border-left: 3px solid var(--fs-blue);
    background: #f7f4f4;
    padding: .7rem .9rem;
    border-radius: 0 var(--fs-radius-sm) var(--fs-radius-sm) 0;
    font-size: .86rem;
}

.fs-note--warn { border-left-color: var(--fs-amber); background: var(--fs-amber-soft); }
.fs-note--danger { border-left-color: var(--fs-red); background: var(--fs-red-soft); }

/* ------------------------------------------------------------------ Tables */

.fs-table { width: 100%; border-collapse: collapse; }
.fs-table th {
    text-align: left;
    font-size: .74rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--fs-muted);
    font-weight: 600;
    padding: .6rem 1.1rem;
    border-bottom: 1px solid var(--fs-line);
    background: var(--fs-neutral-soft);
}
.fs-table td { padding: .72rem 1.1rem; border-bottom: 1px solid var(--fs-line-soft); vertical-align: middle; }
.fs-table tbody tr { transition: background-color .1s ease; }
.fs-table tbody tr:hover { background: var(--fs-line-soft); }
.fs-table tbody tr:last-child td { border-bottom: none; }

/* The first column of a table is nearly always the name of the thing the row is
   about, and it should be the part the eye lands on. */
.fs-table tbody td:first-child { color: var(--fs-ink); font-weight: 550; }

/* ------------------------------------------------------- Sign-in / centred */

.fs-centre {
    min-height: 100vh;
    display: grid;
    place-items: center;
    background:
        radial-gradient(ellipse at 20% 0%, rgba(47, 95, 224, .35) 0%, transparent 55%),
        linear-gradient(160deg, var(--fs-navy) 0%, #0a2a5c 100%);
    padding: 1.5rem;
}

.fs-signin {
    width: 100%;
    max-width: 400px;
    background: #fff;
    border-radius: 16px;
    box-shadow: var(--fs-shadow-lg);
    padding: 2.25rem;
}

/* --------------------------------------------------- Buttons and controls */

.btn {
    border-radius: var(--fs-radius-control);
    font-weight: 550;
    letter-spacing: -.005em;
    transition: background-color .12s ease, border-color .12s ease, box-shadow .12s ease;
}

.btn-primary {
    background: var(--fs-blue);
    border-color: var(--fs-blue);
    box-shadow: 0 1px 2px rgba(15, 23, 41, .12);
}

.btn-primary:hover, .btn-primary:focus {
    background: var(--fs-blue-dark);
    border-color: var(--fs-blue-dark);
}

/* A secondary action should read as an action, not as a disabled one. The default
   outline is too faint against a white card to look clickable at a glance. */
/* Navy text, per the brand: a secondary action is still an action, and at
   --fs-body it read as a line of prose someone had drawn a box around. */
.btn-outline-secondary {
    border-color: var(--fs-line-strong);
    color: var(--fs-navy);
}

.btn-outline-secondary:hover {
    background: var(--fs-neutral);
    border-color: var(--fs-navy);
    color: var(--fs-navy);
}

/* An inactive control recedes to the neutral rather than to white — on this
   canvas white is the page, so a disabled input drawn white is invisible. */
.form-control:disabled, .form-select:disabled,
.btn:disabled, .btn.disabled {
    background-color: var(--fs-neutral);
    border-color: var(--fs-line);

    /* The foreground has to come with the background. A disabled .btn-primary
       keeps its white text otherwise, and white on the neutral is 1.2:1 -- not a
       button that looks unavailable, a button with nothing written on it. */
    color: var(--fs-muted);
    opacity: 1;
}

/* Touch targets during a live observation (spec section 96). */
/*
    A hit area big enough for a thumb, on anything.

    The height alone was not enough: Bootstrap centres a button's label with the
    button's own layout but an <a class="btn"> only has padding, so a link forced to
    44px sat with its text at the top of the box — visibly out of line beside the
    buttons next to it in a row of actions. Centring it here fixes every such row at
    once rather than one at a time.
*/
.fs-touch {
    min-height: 44px;
    min-width: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .3rem;
}

.form-label { font-weight: 600; font-size: .85rem; color: var(--fs-ink); margin-bottom: .3rem; }

/* Deliberately the stronger line, not the hairline used between rows. A field the
   evaluator is about to type into has to be visible as a field in a bright room —
   this is the third time the faintness of these edges has come up. */
.form-control, .form-select {
    border-color: var(--fs-line-strong);
    border-radius: var(--fs-radius-control);
    background-color: var(--fs-surface);
}

.form-control:hover, .form-select:hover { border-color: var(--fs-muted); }

.form-control:focus, .form-select:focus {
    border-color: var(--fs-blue);
    background-color: #fff;
    box-shadow: 0 0 0 3px rgba(47, 95, 224, .15);
}

.form-control::placeholder { color: #a49c98; }

/* Checkboxes and radios.
   Bootstrap outlines these in a 1px 25%-black hairline that all but vanishes on a
   white card. They are the smallest controls on the page and the ones a reader has
   to scan a column of, so they get the strong line and a little more weight. */
.form-check-input {
    border: 1.5px solid var(--fs-line-strong);
    width: 1.05em;
    height: 1.05em;
}

.form-check-input:hover:not(:checked):not(:disabled) { border-color: var(--fs-muted); }
.form-check-input:checked { border-color: var(--fs-blue); }
.form-check-input:focus { border-color: var(--fs-blue); box-shadow: 0 0 0 3px rgba(47, 95, 224, .15); }
.form-check-input:disabled { border-color: var(--fs-line); opacity: .55; }

:focus-visible { outline: 2px solid var(--fs-blue); outline-offset: 2px; }

.visually-hidden-focusable:not(:focus):not(:focus-within) {
    position: absolute !important;
    width: 1px; height: 1px;
    overflow: hidden; clip: rect(0 0 0 0);
}

/* =====================================================================
   Narrow screens (spec section 93).

   The rule everywhere below: the page may scroll down, never sideways.
   A horizontally scrolling personnel record is not a cosmetic problem —
   it hides content that the reader has no reason to suspect is there.
   ===================================================================== */

/*
    Where the sections stop sharing a row with the brand.

    Measured rather than picked: with a full administrator's eleven sections the
    wrap runs to two rows down to about 1150px, and to three or more below that —
    six rows and 242px tall at 768px, a quarter of an iPad's screen spent before the
    page began. Its own row costs a flat 104px whatever the width.

    So the two swap over at the point the wrap stops being cheaper, which is inside
    the band this stylesheet already breaks at elsewhere. Above it the wrap wins by
    about 10px and every section stays in sight; below it the row wins by 30px and
    grows from there. Somebody with fewer permissions has a narrower nav that never
    needs to scroll at all.
*/
@media (max-width: 1199.98px) {
    .app-header__inner {
        flex-wrap: wrap;
        gap: .35rem 1rem;
        padding-top: .35rem;
        padding-bottom: .35rem;

        /* The 56px floor is there to give the brand room on a single-row header.
           With the sections on a row of their own it only pads the gap between the
           two, which cost more height than the wrap it replaced. */
        min-height: 0;
    }

    /* The strip and its menu button take the row together. */
    .app-navwrap {
        order: 3;
        flex: 1 0 100%;
    }

    .app-nav {
        flex-wrap: nowrap;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
    }

    .app-user { margin-left: auto; min-width: 0; }
}

@media (max-width: 767.98px) {
    .app-header__inner { gap: .5rem; padding: .4rem .75rem; }

    /* The name is a courtesy; the sign-out button is the function. Drop the
       name before letting either push the page off-screen. */
    .app-user__name { display: none; }

    .app-main { padding: 1rem .75rem 2rem; }

    /* Tables are the usual culprit: let them scroll in their own box. */
    .fs-table { display: block; overflow-x: auto; white-space: nowrap; }

    .fs-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* -------------------------------------------------- Searchable option list */

.fs-picker {
    border: 1px solid var(--fs-line);
    border-radius: var(--fs-radius-sm);
    background: #fff;
    overflow: hidden;
}

.fs-picker__search { position: relative; border-bottom: 1px solid var(--fs-line-soft); }

.fs-picker__search i {
    position: absolute;
    left: .6rem;
    top: 50%;
    transform: translateY(-50%);
    color: var(--fs-muted);
    font-size: .8rem;
    pointer-events: none;
}

.fs-picker__search input {
    border: none;
    border-radius: 0;
    padding-left: 1.9rem;
}

.fs-picker__search input:focus { box-shadow: none; border-color: transparent; }

.fs-picker__list { max-height: 160px; overflow-y: auto; padding: .45rem .65rem; }
.fs-picker__list .form-check { margin-bottom: .15rem; }
.fs-picker__list .form-check:last-child { margin-bottom: 0; }
.fs-picker__empty { padding: .45rem .65rem; margin: 0; }

/* =====================================================================
   Settings: the hub, the staff table, bulk actions, impersonation.
   ===================================================================== */

.fs-settings {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 1rem;
}

.fs-setting {
    display: block;
    background: var(--fs-surface);
    border: 1px solid var(--fs-line);
    border-radius: var(--fs-radius);
    padding: 1rem 1.1rem;
    color: var(--fs-body);
    box-shadow: var(--fs-shadow);
}

.fs-setting:hover {
    text-decoration: none;
    border-color: var(--fs-blue);
    color: var(--fs-body);
    box-shadow: var(--fs-shadow-lg);
}

.fs-setting__head { display: flex; align-items: center; gap: .55rem; margin-bottom: .4rem; }
.fs-setting__head i { color: var(--fs-blue); font-size: 1.05rem; }
.fs-setting__head h2 { font-size: .98rem; font-weight: 600; color: var(--fs-ink); margin: 0; }
.fs-setting__head .fs-pill { margin-left: auto; }
.fs-setting p { font-size: .84rem; line-height: 1.5; margin: 0; color: var(--fs-muted); }

/* --- The staff table ------------------------------------------------- */

/* Wide by design. It scrolls inside its own box rather than making the page
   scroll sideways, which would hide columns without saying so. */
.fs-table-scroll { overflow-x: auto; }

/*
    A table is the one thing on these pages wide enough to push the whole page
    sideways, and only one of the twenty-seven views that use one wraps it in the
    scroller above. So the card it sits in becomes that scroller instead.

    Not display:block on the table itself, which is the usual trick for this: that
    stops the columns sharing the width and leaves them at their content size — a
    two-column table went from a 346px header to a 44px one, with the rest of the
    table empty beside it. This keeps the table a table.

    Setting overflow-x here makes overflow-y compute to auto as well, which the spec
    allows no way around, and a scroll container clips a dropdown that escapes it.
    None of the twenty-seven has one inside a table card today; anything added later
    needs Popper's fixed strategy to hang outside.
*/
.fs-card:has(> .fs-table),
.fs-card__body:has(> .fs-table) { overflow: auto; }

/* =====================================================================
   The staff editor on the users table.

   A person's record is a dozen fields, two lists of names and an account
   control, and none of that fits in the eighty pixels an actions column
   can spare. So the toggle stays in the cell and the editor is the row
   underneath it, opened by the toggle's own state rather than by script —
   there is no inline JavaScript on these pages.
   ===================================================================== */

.du-toggle > summary { list-style: none; }
.du-toggle > summary::-webkit-details-marker { display: none; }

.du-editor-row { display: none; }

/* The editor row is the sibling of whichever row holds an open toggle. */
tr:has(.du-toggle[open]) + .du-editor-row { display: table-row; }

/* And the row above it reads as the one being edited. */
tr:has(.du-toggle[open]) { background: var(--fs-blue-soft); }

.du-editor {
    padding: 1.25rem 1.1rem 1.4rem;
    background: var(--fs-canvas);
    border-top: 2px solid var(--fs-blue);
    white-space: normal;
}

.du-editor__title {
    font-size: .82rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--fs-muted);
    margin: 0 0 .35rem;
}

/* Two columns of groups rather than six stacked rows of one radio each. */
.du-groups {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: .1rem 1rem;
}

/* The two lists of names — supervisors above, direct reports below. */
.du-links { list-style: none; margin: 0 0 .75rem; padding: 0; }

.du-links li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    padding: .4rem .6rem;
    background: var(--fs-surface);
    border: 1px solid var(--fs-line-soft);
    border-radius: var(--fs-radius-sm);
}

.du-links li + li { margin-top: .3rem; }
.du-links form { margin: 0; }

/*
    The actions on one row of a list of people.

    Icons rather than words: a name with "Open" and "Remove" spelled out beside it
    is three things competing to be read first, and with forty rows the words are
    the only thing the eye lands on. Kept side by side — stacked, they doubled the
    height of every row.
*/
.du-links__actions {
    display: flex;
    align-items: center;
    gap: .25rem;
    flex: none;
}

.du-icon {
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    flex: none;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--fs-radius-sm);
    background: none;
    color: var(--fs-muted);
    line-height: 1;
    text-decoration: none;
}

.du-links li:hover .du-icon { border-color: var(--fs-line); }

.du-icon:hover {
    background: var(--fs-neutral);
    color: var(--fs-ink);
    text-decoration: none;
}

.du-icon:focus-visible {
    outline: 2px solid var(--fs-navy);
    outline-offset: 1px;
}

/* Ending an assignment is the one here worth a second look before it is pressed. */
.du-icon--danger:hover {
    background: var(--fs-red-soft);
    color: var(--fs-red);
    border-color: var(--fs-red);
}

/* "Nobody" is a real answer and worth reading as one rather than as an
   empty space somebody has to interpret. */
.du-none { font-style: italic; }

.du-add { display: grid; gap: .35rem; }

/* =====================================================================
   A member of staff's folder — what each year asked for, and what
   was done.
   ===================================================================== */

/* The year picker sits in the card head beside the title rather than above
   the table, so switching year does not move the thing you are reading. */
.fs-folder__year {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin: 0;
}

.fs-folder__year select { min-width: 15rem; }

.fs-folder__section + .fs-folder__section { border-top: 1px solid var(--fs-line); }

.fs-folder__sectionHead {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    padding: .9rem 1.1rem .55rem;
}

.fs-folder__sectionHead h3 {
    margin: 0;
    font-size: .95rem;
    font-weight: 700;
}

/* A requirement met several times opens in place. The marker is the district's
   own disclosure triangle rather than the browser's, which sits at a different
   height in every one of them. */
.fs-folder__group > summary {
    display: flex;
    align-items: center;
    gap: .5rem;
    list-style: none;
    cursor: pointer;
    padding: .1rem 0;
}

.fs-folder__group > summary::-webkit-details-marker { display: none; }

.fs-folder__group > summary::before {
    content: "";
    flex: none;
    width: 0;
    height: 0;
    border-style: solid;
    border-width: .32rem 0 .32rem .46rem;
    border-color: transparent transparent transparent var(--fs-muted);
    transition: transform .15s ease;
}

.fs-folder__group[open] > summary::before { transform: rotate(90deg); }

/* The occurrences are a table inside a cell, so they need their own edges back:
   flush left with the row above and separated from it, not floating. */
.fs-folder__inner {
    margin: .5rem 0 .25rem;
    background: var(--fs-canvas);
    border: 1px solid var(--fs-line-soft);
    border-radius: var(--fs-radius-sm);
}

.fs-folder__inner td { border-bottom: 1px solid var(--fs-line-soft); }
.fs-folder__inner tr:last-child td { border-bottom: 0; }

.fs-table--dense th,
.fs-table--dense td { padding: .45rem .6rem; font-size: .84rem; vertical-align: middle; }
.fs-table--dense thead th { white-space: nowrap; }
.fs-table--dense thead a { color: inherit; }
.fs-table--dense thead a:hover { color: var(--fs-blue-dark); }
.fs-table--dense .fs-pill { margin-right: .15rem; }

.fs-row--inactive { opacity: .6; }

/* --- Bulk action bar -------------------------------------------------- */

.fs-bulk {
    position: sticky;

    /* Below the header, not behind it. site.js measures the header because its
       height changes when the navigation wraps; the fallback keeps the bar on
       screen if that has not run yet. */
    top: var(--fs-header-h, 4.5rem);
    z-index: 5;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .6rem;
    background: var(--fs-navy-soft);
    color: #eaf0f8;
    border-radius: var(--fs-radius);
    padding: .6rem .85rem;
    margin-bottom: .75rem;
    box-shadow: var(--fs-shadow-lg);
}

.fs-bulk__count { font-size: .85rem; }
.fs-bulk__count strong { font-size: 1rem; }
.fs-bulk__count .btn-link { color: #b8b1ad; text-decoration: underline; }

.fs-bulk__scope { font-size: .82rem; color: #c6d5e8; }
.fs-bulk__scope .form-check-input { margin-right: .3rem; }

.fs-bulk__actions { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; margin-left: auto; }
.fs-bulk__actions .form-select,
.fs-bulk__actions .form-control { width: auto; min-width: 9rem; }

/* --- Impersonation --------------------------------------------------- */

/*
   Deliberately loud, deliberately at the very top, and deliberately not
   dismissible. Somebody who forgets they are inside another person's account
   can do real damage to a personnel record without meaning to.
*/
.app-impersonation {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    background: var(--fs-amber);
    color: #fff;
    padding: .5rem 1.25rem;
    font-size: .88rem;
    font-weight: 500;
}

.app-impersonation strong { font-weight: 700; }

/* A permanent border around the whole page, so the state is visible even when
   the banner itself has been scrolled past. */
body.is-impersonating { box-shadow: inset 0 0 0 3px var(--fs-amber); }

/* --- Small inline edit forms ----------------------------------------- */

.fs-inline-form {
    border: 1px solid var(--fs-line);
    border-radius: var(--fs-radius-sm);
    padding: .6rem;
    background: #fbf9fa;
    min-width: 15rem;
}

/* ------------------------------------------------------------ Notifications */

.app-bell {
    position: relative;
    color: #c6d5e8;
    padding: .3rem .45rem;
    border-radius: var(--fs-radius-sm);
    line-height: 1;
}

.app-bell:hover { background: rgba(255, 255, 255, .09); color: #fff; }

/*
    Gold, and navy on it rather than white.

    This is exactly what the accent is for — a notification dot on the navy bar —
    and the pairing is 9.7:1. White on gold would be 1.9:1 and unreadable, which
    is the trap the whole gold rule exists to avoid.
*/
.app-bell__count {
    position: absolute;
    top: -2px;
    right: -4px;
    background: var(--fs-gold);
    color: var(--fs-navy);
    border-radius: 999px;
    font-size: .62rem;
    font-weight: 700;
    line-height: 1;
    padding: .15rem .3rem;
    min-width: 1rem;
    text-align: center;
}

/* --- The rule cards --------------------------------------------------- */

.fs-rules {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
    gap: 1rem;
    align-items: start;
}

.fs-rule { padding: .9rem 1rem 1rem; }

/* An enabled rule is visibly different from one that is merely configured —
   the question an administrator is answering is "what actually fires?" */
.fs-rule--on { border-left: 3px solid var(--fs-blue); }

.fs-rule__head { display: flex; align-items: center; gap: .5rem; }
.fs-rule__head .fs-pill { margin-left: auto; }
.fs-rule__what { margin: .3rem 0 .8rem; }

.fs-rule__body {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 1rem;
}

.fs-rule__group .form-check { margin-bottom: .35rem; }
.fs-rule__group .form-check-label { font-size: .84rem; }
.fs-rule__group .fs-hint { font-size: .74rem; }

.fs-rule__foot {
    display: flex;
    justify-content: flex-end;
    margin-top: .9rem;
    padding-top: .7rem;
    border-top: 1px solid var(--fs-line-soft);
}

/* --- The inbox -------------------------------------------------------- */

.fs-notice {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: .75rem 1rem;
    border-bottom: 1px solid var(--fs-line-soft);
}

.fs-notice:last-child { border-bottom: none; }
.fs-notice--unread { background: #faf8f8; border-left: 3px solid var(--fs-blue); }
.fs-notice__body { flex: 1; min-width: 0; }
.fs-notice__body time { display: block; font-size: .74rem; margin-top: .2rem; }

/* -------------------------------------------------- Back to Settings, tiles */

.fs-back {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    font-size: .82rem;
    color: var(--fs-muted);
    margin-bottom: .35rem;
}

.fs-back:hover { color: var(--fs-blue-dark); text-decoration: none; }
.fs-back i { font-size: .75rem; }

/*
    A stat tile that leads somewhere.

    A number an administrator cannot open is a number they have to go and look for
    somewhere else, so every count that has a list behind it is a link. The lift on
    hover is the only signal needed — the cursor and the underline do the rest.
*/
a.fs-stat { display: block; color: inherit; transition: border-color .12s ease, box-shadow .12s ease; }

a.fs-stat:hover {
    text-decoration: none;
    color: inherit;
    border-color: var(--fs-blue);
    box-shadow: var(--fs-shadow-lg);
}

a.fs-stat:hover .fs-stat__label { color: var(--fs-blue-dark); }

/* Says where it goes without adding a second line of text. */
a.fs-stat .fs-stat__label::after {
    content: " ›";
    font-weight: 700;
    opacity: 0;
    transition: opacity .12s ease;
}

a.fs-stat:hover .fs-stat__label::after { opacity: 1; }

/* Rubric entry: the four levels side by side, so the progression reads across. */
.fs-rubric {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: .75rem;
}

.fs-rubric__level .form-label { font-weight: 600; font-size: .85rem; }
.fs-rubric__level textarea { font-size: .84rem; line-height: 1.45; }

/* ------------------------------------------------ Choosing a member of staff */

.fs-people { max-height: 60vh; overflow-y: auto; }

.fs-person {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .7rem 1rem;
    border-bottom: 1px solid var(--fs-line-soft);
    color: var(--fs-body);
}

.fs-person:last-child { border-bottom: none; }
.fs-person:hover { background: #faf8f8; text-decoration: none; color: var(--fs-body); }

.fs-person__name { display: flex; flex-direction: column; flex: 1; min-width: 0; line-height: 1.3; }
.fs-person__name .fs-hint { font-size: .78rem; }
.fs-person__go { color: var(--fs-blue-dark); font-size: .84rem; font-weight: 600; white-space: nowrap; }

/* --------------------------------------------------------- The year's plan */

/*
   Deliberately a list rather than cards. An evaluator standing in a corridor is
   scanning for the next thing to do, and a list of rows is far quicker to scan
   than a grid of boxes.
*/
.fs-plan__group {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .55rem 1rem;
    background: var(--fs-line-soft);
    border-bottom: 1px solid var(--fs-line);
    font-size: .85rem;
}

.fs-plan__group i { color: var(--fs-muted); }
.fs-plan__group-title { font-weight: 600; color: var(--fs-ink); }

.fs-plan__row {
    display: flex;
    align-items: center;
    gap: .65rem;
    padding: .6rem 1rem;
    border-bottom: 1px solid var(--fs-line-soft);
}

.fs-plan__row:last-child { border-bottom: none; }
.fs-plan__row:hover { background: #fbf9fa; }

/* Indented so a repeated step reads as belonging to its group. */
.fs-plan__row--child { padding-left: 2rem; }
.fs-plan__row--done .fs-plan__title { color: var(--fs-muted); }

.fs-plan__seq {
    min-width: 1.6rem;
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-size: .8rem;
    color: var(--fs-muted);
}

.fs-plan__icon { color: var(--fs-muted); font-size: .95rem; }
.fs-plan__title { flex: 1; min-width: 0; font-size: .9rem; color: var(--fs-ink); }
.fs-plan__due { white-space: nowrap; font-size: .78rem; }
.fs-plan__status { white-space: nowrap; }
.fs-plan__action { white-space: nowrap; display: flex; gap: .35rem; align-items: center; }

/* The booked time, sitting under the item it belongs to. */
.fs-plan__booked {
    display: block;
    font-size: .78rem;
    color: var(--fs-muted);
    margin-top: .15rem;
}

.fs-plan__booked i { margin-right: .2rem; }

/* Required is marked the way the district's own paperwork marks it. */
.fs-plan__required { color: var(--fs-red); font-weight: 700; margin-left: .15rem; }

@media (max-width: 575.98px) {
    .fs-plan__row { flex-wrap: wrap; }
    .fs-plan__title { flex: 1 1 60%; }
    .fs-plan__due { order: 5; }
}

/* ---------------------------------------------------------- District targets */

.fs-goal__quarter + .fs-goal__quarter { margin-top: 1.5rem; }

.fs-goal__quarter-title {
    font-size: .78rem;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--fs-muted);
    margin: 0 0 .5rem;
}

.fs-goal__block + .fs-goal__block { margin-top: 1rem; }

.fs-goal__header {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin-bottom: .35rem;
}

.fs-goal__label { font-size: .85rem; font-weight: 600; color: var(--fs-ink); }

.fs-goal__row {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .2rem 0;
}

.fs-goal__school {
    flex: 0 0 7.5rem;
    font-size: .82rem;
    color: var(--fs-ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.fs-goal__track {
    position: relative;
    flex: 1 1 auto;
    min-width: 0;
    height: .7rem;
    border-radius: 999px;
    background: #f1eeed;
    overflow: hidden;
}

.fs-goal__fill {
    position: absolute;
    inset: 0 auto 0 0;
    border-radius: 999px;
    background: var(--fs-muted);
}

.fs-goal__fill.is-met { background: #17803D; }
.fs-goal__fill.is-behind { background: #B45309; }
.fs-goal__fill.is-missed { background: #B42318; }

/* Where the district asked to be, drawn over the bar rather than beside it. */
.fs-goal__target {
    position: absolute;
    top: -2px;
    bottom: -2px;
    width: 2px;
    background: var(--fs-ink);
    opacity: .55;
}

.fs-goal__value {
    flex: 0 0 auto;
    font-size: .8rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

@media (max-width: 575.98px) {
    .fs-goal__school { flex-basis: 4.5rem; }
    .fs-goal__value .fs-hint { display: none; }
}

/* A plan section header is a button, but reads as a folder row. */
.fs-plan__group {
    width: 100%;
    border: none;
    border-bottom: 1px solid var(--fs-line, #ebe6e4);
    text-align: left;
    font: inherit;
    cursor: pointer;
}

.fs-plan__group:hover { background: #f1eeed; }

.fs-plan__group:focus-visible {
    outline: 2px solid var(--fs-accent, #17457f);
    outline-offset: -2px;
}

.fs-plan__chevron {
    color: var(--fs-muted);
    font-size: .8rem;
    width: .9rem;
    flex: 0 0 auto;
}

.fs-plan__children[hidden] { display: none; }

.fs-note--ok { border-left-color: var(--fs-green); background: var(--fs-green-soft); }
.fs-note--info { border-left-color: var(--fs-blue); background: #f7f4f4; }

/* ------------------------------- The record as the member of staff reads it */

.fs-shared__stream > * + * { border-top: 1px solid var(--fs-line-soft); }

.fs-shared__entry { padding: .7rem 0; }
.fs-shared__entry:first-child { padding-top: 0; }

.fs-shared__entry header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .4rem;
    margin-bottom: .3rem;
}

.fs-shared__entry time {
    font-size: .78rem;
    color: var(--fs-muted);
    font-variant-numeric: tabular-nums;
}

.fs-shared__body { font-size: .9rem; color: var(--fs-ink); }
.fs-shared__body p:last-child { margin-bottom: 0; }

.fs-shared__feedback + .fs-shared__feedback { margin-top: 1rem; }

.fs-shared__feedback h3,
.fs-shared__closing h3 {
    font-size: .78rem;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--fs-muted);
    margin: 0 0 .25rem;
}

.fs-shared__closing {
    border-top: 1px solid var(--fs-line-soft);
    padding-top: 1rem;
}

/* The footer an acknowledgement is made from: the sentence that qualifies the act,
   then the buttons. Padded like a card body, because it is one — it just carries a
   rule above it to separate the deliberate act from the box above. */
.fs-shared__signoff {
    border-top: 1px solid var(--fs-line);
    padding: 1rem 1.1rem 1.1rem;
}

/* Stacked, not in a row. In the side column three facts across wrapped mid-value
   and read as one run-on line. */
.fs-shared__meta {
    display: grid;
    gap: .75rem;
    margin: 0;
}

.fs-shared__meta dt {
    font-size: .7rem;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--fs-muted);
}

.fs-shared__meta dd { margin: 0; font-size: .86rem; color: var(--fs-ink); }

/* Three steps, and which one it is sitting on. */
.fs-steps { list-style: none; margin: 0; padding: 0; }

.fs-steps li {
    position: relative;
    padding: 0 0 .9rem 1.4rem;
    border-left: 2px solid var(--fs-line);
    font-size: .85rem;
}

.fs-steps li:last-child { padding-bottom: 0; border-left-color: transparent; }

.fs-steps li::before {
    content: "";
    position: absolute;
    left: -6px;
    top: .25rem;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: #fff;
    border: 2px solid var(--fs-line);
}

.fs-steps li.is-done::before { background: var(--fs-green); border-color: var(--fs-green); }
.fs-steps li.is-current::before { background: var(--fs-blue); border-color: var(--fs-blue); }
.fs-steps li.is-current { font-weight: 600; }

.fs-steps strong { display: block; font-weight: inherit; }

/* ------------------------------------------- Walkthrough critical attributes */

/*
    The critical attributes under a component on the walkthrough form.

    Set apart as reference rather than run in as a paragraph, because they are the
    one thing on that screen an evaluator scans rather than reads — and labelled
    "guidance" beneath, because the district's rule is that they are examples and
    not a checklist a teacher has to complete in four minutes.
*/
.fs-attributes {
    border-left: 3px solid var(--fs-line-strong);
    background: var(--fs-line-soft);
    border-radius: 0 var(--fs-radius-sm) var(--fs-radius-sm) 0;
    padding: .7rem .9rem;
}

.fs-attributes__label {
    display: block;
    font-size: .68rem;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--fs-muted);
    margin-bottom: .35rem;
}

.fs-attributes ul { margin: 0; padding-left: 1.1rem; }
.fs-attributes li { margin-bottom: .2rem; }
.fs-attributes li:last-child { margin-bottom: 0; }

/* ---------------------------------------------------------- Choice buttons */

/*
    A segmented control built on radio inputs.

    The input stays in the DOM and keeps the name/checked semantics — arrow-key
    navigation, one-of-many, and correct behaviour with CSS off — while the label
    does the drawing. Faking this with buttons and a hidden value would break every
    one of those.
*/
.fs-choice { display: inline-flex; flex-wrap: wrap; gap: .5rem; }

.fs-choice__input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

.fs-choice__button {
    display: inline-flex;
    flex-direction: column;
    justify-content: center;
    min-height: 44px;
    padding: .5rem 1.1rem;
    border: 1.5px solid var(--fs-line-strong);
    border-radius: var(--fs-radius-sm);
    background: var(--fs-surface);
    color: var(--fs-body);
    font-weight: 550;
    cursor: pointer;
    transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}

.fs-choice__button small { font-weight: 400; font-size: .78rem; color: var(--fs-muted); }
.fs-choice__button:hover { border-color: var(--fs-blue); background: var(--fs-blue-soft); }

.fs-choice__input:checked + .fs-choice__button {
    background: var(--fs-blue);
    border-color: var(--fs-blue);
    color: #fff;
    box-shadow: 0 1px 2px rgba(15, 23, 41, .18);
}

.fs-choice__input:checked + .fs-choice__button small { color: rgba(255, 255, 255, .8); }

/* The input is invisible, so its focus ring has to be drawn on the label or a
   keyboard user has no idea where they are. */
.fs-choice__input:focus-visible + .fs-choice__button {
    outline: 2px solid var(--fs-blue);
    outline-offset: 2px;
}

.fs-choice__input:disabled + .fs-choice__button {
    opacity: .6;
    cursor: default;
    border-color: var(--fs-line);
}

/* ------------------------------------------------------- Question attachments */

.fs-files__list { list-style: none; margin: 0 0 .5rem; padding: 0; }

.fs-files__item {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .45rem .6rem;
    border: 1px solid var(--fs-line);
    border-radius: var(--fs-radius-sm);
    margin-bottom: .35rem;
    background: var(--fs-surface);
}

.fs-files__icon { color: var(--fs-muted); flex: none; }
.fs-files__name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.fs-files__size { font-size: .78rem; color: var(--fs-muted); white-space: nowrap; }

.fs-files__remove {
    border: 0;
    background: none;
    padding: 0;
    color: var(--fs-red);
    font: inherit;
    font-size: .82rem;
    cursor: pointer;
}

.fs-files__remove:hover { text-decoration: underline; }
.fs-files__empty { font-size: .85rem; color: var(--fs-muted); margin-bottom: .5rem; }

/* Dashed, because a drop target that looks like a card looks like content. */
.fs-files__drop {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .25rem;
    padding: 1.1rem;
    border: 2px dashed var(--fs-line-strong);
    border-radius: var(--fs-radius-sm);
    background: var(--fs-canvas);
    color: var(--fs-muted);
    text-align: center;
    cursor: pointer;
}

.fs-files__drop i { font-size: 1.3rem; }
.fs-files__drop strong { color: var(--fs-body); }
.fs-files__drop small { font-size: .75rem; }
.fs-files__drop:hover { border-color: var(--fs-blue); background: var(--fs-blue-soft); }
.fs-files__drop:focus-visible { outline: 2px solid var(--fs-blue); outline-offset: 2px; }

.fs-files__drop.is-over { border-color: var(--fs-blue); background: var(--fs-blue-soft); }
.fs-files__drop.is-busy { opacity: .6; pointer-events: none; }

.fs-files__error { margin: .5rem 0 0; font-size: .85rem; color: var(--fs-red); }

/* -------------------------------------------------------------- Form actions */

/*
    The action bar under a form.

    Beside the form, Submit sat level with the first question — on a form of ten
    sections, where nobody has finished reading. It belongs at the end, where the
    work ends.
*/
.fp-actions { margin-top: 1rem; }

.fp-actions__row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .75rem;
}

.fp-actions__buttons { display: flex; flex-wrap: wrap; gap: .6rem; margin-left: auto; }
.fp-actions__ack { margin-top: 1.1rem; padding-top: 1.1rem; border-top: 1px solid var(--fs-line); max-width: 40rem; }

@media (max-width: 575.98px) {
    .fp-actions__buttons { margin-left: 0; width: 100%; }
    .fp-actions__buttons .btn { flex: 1 1 auto; }
}


/* ------------------------------------------------------- Component ratings */

/*
    The rubric, laid out the way the district's own document lays it out:
    considerations down the left, the four performance levels across.

    Every column is a grid row of its own — button, then rubric text — so the buttons
    line up across the four levels however unevenly the wording wraps. Reading down a
    column and comparing across a row both work, which is how a rubric is actually
    used.
*/
.fp-rating__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    margin-bottom: .75rem;
}

.fp-rubric {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    grid-template-rows: auto 1fr;
    gap: 0;
    border: 1px solid var(--fs-line);
    border-radius: var(--fs-radius);
    background: var(--fs-surface);
    overflow: hidden;
}

/* The considerations column is narrower — it asks the questions, it does not carry
   the same weight of text. */
.fp-rubric--guided { grid-template-columns: minmax(0, 0.85fr) repeat(4, minmax(0, 1fr)); }

.fp-rubric__aside {
    grid-row: 1 / -1;
    padding: 1rem;
    border-right: 1px solid var(--fs-line);
    background: var(--fs-canvas);
}

.fp-rubric__asideTitle {
    margin: 0 0 .75rem;
    font-size: 1rem;
    font-weight: 700;
    color: var(--fs-ink);
}

.fp-rubric__asideBody { font-size: .82rem; line-height: 1.55; color: var(--fs-body); }
.fp-rubric__asideBody ul { margin: 0; padding-left: 1.1rem; }
.fp-rubric__asideBody li + li { margin-top: .6rem; }

/* Each level is its own two-row subgrid so buttons align across columns. */
.fp-rubric__level {
    display: grid;
    grid-row: 1 / -1;
    grid-template-rows: subgrid;
    border-left: 1px solid var(--fs-line);
}

.fp-rubric__level:first-child { border-left: 0; }
.fp-rubric--guided .fp-rubric__level:nth-child(2) { border-left: 0; }

.fp-rubric__input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

.fp-rubric__button {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    margin: .55rem;
    padding: .5rem .75rem;
    border: 1.5px solid var(--fs-line-strong);
    border-radius: var(--fs-radius-sm);
    background: var(--fs-surface);
    color: var(--fs-body);
    font-weight: 650;
    text-align: center;
    cursor: pointer;
    transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}

.fp-rubric__button:hover { border-color: var(--fs-blue); background: var(--fs-blue-soft); }

.fp-rubric__input:checked + .fp-rubric__button {
    background: var(--fs-blue);
    border-color: var(--fs-blue);
    color: #fff;
}

/* The input is invisible, so the focus ring has to live on the label or a keyboard
   user cannot tell where they are. */
.fp-rubric__input:focus-visible + .fp-rubric__button {
    outline: 2px solid var(--fs-blue);
    outline-offset: 2px;
}

.fp-rubric__input:disabled + .fp-rubric__button { opacity: .65; cursor: default; }

.fp-rubric__text {
    padding: 0 .85rem 1rem;
    font-size: .8rem;
    line-height: 1.55;
    color: var(--fs-body);
}

.fp-rubric__text p { margin: 0 0 .7rem; }
.fp-rubric__text h4 { margin: .9rem 0 .35rem; font-size: .8rem; font-weight: 700; color: var(--fs-ink); }
.fp-rubric__text ul { margin: 0 0 .7rem; padding-left: 1.05rem; }
.fp-rubric__text li + li { margin-top: .5rem; }
.fp-rubric__text > :last-child { margin-bottom: 0; }

/*
    Collapsed, the considerations fold away entirely and the four levels spread into
    the space. Leaving an all-but-empty column behind would cost the width without
    showing anything, and the questions are the tallest thing on a collapsed rubric —
    keeping them would have made collapsing the detail pointless.
*/
.fp-rubric--guided:not(.is-open) { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.fp-rubric--guided:not(.is-open) .fp-rubric__aside { display: none; }
.fp-rubric--guided:not(.is-open) .fp-rubric__level:first-of-type { border-left: 0; }

/* The chosen column stays marked all the way down, not just at its button. */
.fp-rubric__input:checked ~ .fp-rubric__text { background: var(--fs-blue-soft); }

/*
    Below a laptop the five columns stop being readable side by side. The rubric
    becomes a stack of levels, each still headed by its own button, and the
    considerations move to the top where they still frame all four.
*/
@media (max-width: 1199.98px) {
    /*
        .fp-rubric--guided:not(.is-open) is named because the closed-state rule above
        sets four columns at (0,2,0) and a media query carries no specificity of its
        own — without the matching selector here the rubric never stacked at any
        width. At 390px that left four 77px columns inside overflow:hidden, so the
        level descriptors an evaluator rates against were clipped and unreachable.
    */
    .fp-rubric,
    .fp-rubric--guided,
    .fp-rubric--guided:not(.is-open) { grid-template-columns: minmax(0, 1fr); grid-template-rows: none; }

    .fp-rubric__aside,
    .fp-rubric__level {
        grid-row: auto;
        grid-template-rows: none;
        border-left: 0;
        border-right: 0;
        border-top: 1px solid var(--fs-line);
    }

    .fp-rubric__aside { border-top: 0; }
}

/* The calculated average reads as a result, not as something to type in. */
.fs-calc { background: var(--fs-canvas); font-weight: 650; font-variant-numeric: tabular-nums; }

@media print {
    /*
        On paper the fill is the only thing marking the chosen level, and browsers drop
        background colours from print by default. A printed observation whose rating
        cannot be read is not a record of anything, so the fill is forced and a tick is
        added for anyone printing in black and white.
    */
    .fp-rubric__input:checked + .fp-rubric__button {
        -webkit-print-color-adjust: exact;
        print-color-adjust: exact;
        border-width: 2.5px;
    }

    .fp-rubric__input:checked + .fp-rubric__button .fp-rubric__name::before { content: "\2713\00a0"; }

    .fp-rubric__input:not(:checked) + .fp-rubric__button { color: var(--fs-muted); }

    .fp-rating__head { display: none; }

    /* A rubric column split across a page break is unreadable. */
    .fp-rubric__level, .fp-rubric__aside { break-inside: avoid; }
}

/*
    A value the record already holds. Shaped like the inputs beside it so the row reads
    as one row, but plainly not typeable — nobody should try to correct a name here and
    wonder why it will not take.
*/
.fs-known {
    display: flex;
    align-items: center;

    /* The same 44px the inputs beside it use. A row where one box is a few pixels
       shorter than the next reads as a mistake even when nobody can say why. */
    min-height: 44px;
    padding: .375rem .75rem;
    border: 1px solid var(--fs-line);
    border-radius: var(--fs-radius-sm);
    background: var(--fs-canvas);
    color: var(--fs-body);
    font-weight: 600;
}

/* ---------------------------------------------- Rubric detail and district lists */

/*
    The control that opens the specifics under all four levels.

    Full width and directly under the rubric, because what it opens is the whole
    width of the rubric — a small button in a corner would not read as belonging to
    all four columns.
*/
.fp-rubric__toggle {
    display: block;
    width: 100%;
    margin-top: .4rem;
    padding: .5rem;
    border: 1px solid var(--fs-line);
    border-radius: var(--fs-radius-sm);
    background: var(--fs-canvas);
    color: var(--fs-blue);
    font: inherit;
    font-size: .85rem;
    font-weight: 650;
    cursor: pointer;
}

.fp-rubric__toggle:hover { border-color: var(--fs-blue); background: var(--fs-blue-soft); }

.fp-rubric__toggle .fp-rubric__toggleHide { display: none; }
.fp-rubric__toggle[aria-expanded="true"] .fp-rubric__toggleShow { display: none; }
.fp-rubric__toggle[aria-expanded="true"] .fp-rubric__toggleHide { display: inline; }

/* The rubric is a record, so on paper it is always whole — nobody can expand a
   printed page. */
@media print {
    .fp-rubric__toggle { display: none; }
    .fp-rubric__detail { display: block !important; }

    /* Forced past the collapsed rules above, which are the more specific of the two.
       Whether somebody happened to leave the rubric open on screen must not decide
       what the printed record says. */
    .fp-rubric--guided { grid-template-columns: minmax(0, 0.85fr) repeat(4, minmax(0, 1fr)) !important; }
    .fp-rubric--guided .fp-rubric__aside { display: block !important; }
}

/*
    Rows of short fields sit closer together than the default. A row of names, a date
    and a grade is one block of context, not five separate questions with a gap
    between each — and the taller fields below still carry their own spacing.
*/
/* !important because Bootstrap's own .mb-3 carries it, and a plain rule here
   loses however specific the selector is. */
.fp-field > .mb-3 { margin-bottom: .4rem !important; }

/* ------------------------------------------------------------- Dropdowns */

/* One collapsible list. The row is the control; the body only exists when open. */
.fs-dd + .fs-dd { border-top: 1px solid var(--fs-line-soft); }

.fs-dd__head {
    display: flex;
    align-items: center;
    gap: .85rem;
    padding: .9rem 1rem;
    color: inherit;
    text-decoration: none;
}

.fs-dd__head:hover { background: var(--fs-canvas); color: inherit; }
.fs-dd__head:focus-visible { outline: 2px solid var(--fs-blue); outline-offset: -2px; }

.fs-dd__chevron { color: var(--fs-muted); font-size: .8rem; }

.fs-dd__text { display: flex; flex-direction: column; gap: .1rem; min-width: 0; flex: 1; }
.fs-dd__title { font-weight: 600; }

.fs-dd__count { display: flex; align-items: baseline; gap: .35rem; white-space: nowrap; }
.fs-dd__number { font-size: 1.15rem; font-weight: 600; }

/* Open: the head reads as the heading of what follows rather than as another row. */
.fs-dd--open > .fs-dd__head { background: var(--fs-blue-soft); }
.fs-dd--open > .fs-dd__head .fs-dd__chevron { color: var(--fs-blue); }

.fs-dd__body { padding: .85rem 1rem; }

.fs-listadd { display: flex; gap: .5rem; }
.fs-listadd input { flex: 1; }

.fs-list { margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--fs-line-soft); }

.fs-list__row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "name actions" "meta actions";
    align-items: center;
    gap: .25rem .75rem;
    padding: .55rem 1rem;
    border-bottom: 1px solid var(--fs-line-soft);
}

.fs-list__row--retired { background: var(--fs-canvas); }
.fs-list__row--retired .fs-list__name input { color: var(--fs-muted); }

.fs-list__name { grid-area: name; display: flex; gap: .4rem; }
.fs-list__name input { flex: 1; min-width: 0; }

/* The Rename button is only interesting once the name has been touched, so it stays
   out of the way until the field is in use. */
.fs-list__name button { opacity: 0; transition: opacity .12s ease; }
.fs-list__name:hover button,
.fs-list__name:focus-within button { opacity: 1; }

.fs-list__meta { grid-area: meta; display: flex; align-items: center; gap: .4rem; min-height: 0; }
.fs-list__meta:empty { display: none; }

.fs-list__actions { grid-area: actions; display: flex; gap: .25rem; align-items: center; }

/* ------------------------------------------------------ External sign-in */

/* A rule with the word sitting in it, separating the two ways in. */
.fs-signin__or {
    display: flex;
    align-items: center;
    gap: .75rem;
    margin: 1.25rem 0;
    color: var(--fs-muted);
    font-size: .8rem;
}

.fs-signin__or::before,
.fs-signin__or::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--fs-line);
}

.fs-google { display: flex; align-items: center; justify-content: center; gap: .6rem; font-weight: 600; }
.fs-google__mark { display: flex; }

/* A key or token, where the difference between l, 1 and I has to be readable. */
.fs-mono { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-size: .85rem; }

/* --------------------------------------------------------- Assistant output */

/*
    What a model returned, marked off from the district's own screen so nobody mistakes
    a suggestion for something the system is asserting.
*/
.fs-response {
    border: 1px solid var(--fs-line);
    border-left: 3px solid var(--fs-blue);
    border-radius: var(--fs-radius-sm);
    background: var(--fs-canvas);
}

.fs-response__head {
    display: flex;
    align-items: center;
    gap: .6rem;
    flex-wrap: wrap;
    padding: .6rem .85rem;
    border-bottom: 1px solid var(--fs-line);
}

/* pre, so the model's own line breaks survive — but wrapped, because a long answer in
   a single line would scroll the page sideways. */
.fs-response__body {
    margin: 0;
    padding: .85rem;
    font-family: inherit;
    font-size: .9rem;
    line-height: 1.6;
    color: var(--fs-body);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* A sortable column header. Reads as a header first and a control second. */
.fs-sort {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    color: inherit;
    text-decoration: none;
}

.fs-sort:hover { color: var(--fs-navy-soft); text-decoration: underline; }
.fs-sort--on { color: var(--fs-navy); }
.fs-sort i { font-size: .7em; }

/* ------------------------------------------------------------ Reveal blocks */

/*
    A section that opens when it is wanted.

    Used where a screen would otherwise show every form it owns at once — the marker
    is replaced with a caret so the summary reads as a control rather than a bullet.
*/
.fs-reveal > summary {
    cursor: pointer;
    list-style: none;
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    font-size: .85rem;
    font-weight: 600;
    color: var(--fs-blue);
}

.fs-reveal > summary::-webkit-details-marker { display: none; }

.fs-reveal > summary::before {
    content: "";
    width: .4rem;
    height: .4rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(-45deg);
    transition: transform .15s ease;
}

.fs-reveal[open] > summary::before { transform: rotate(45deg); }

.fs-reveal > summary:hover { text-decoration: underline; }

/* Long values in a flex row must be allowed to shrink, or the row grows instead. */
.min-w-0 { min-width: 0; }

/* A per-person job type on the rules preview. Narrow: it sits in a table column. */
.fs-assign { min-width: 8.5rem; }

/* Overridden by hand — marked so the exceptions are visible down the column, not
   only on the row somebody is looking at. */
.fs-assign--changed {
    border-color: var(--fs-blue);
    background-color: var(--fs-blue-soft);
    font-weight: 600;
}

/* Disabled here means "unchanged, so nothing to send" rather than "unavailable", and
   it must not look like the latter. */
.fs-assign:disabled { background-color: var(--fs-surface); color: var(--fs-body); opacity: 1; }

/* ------------------------------------------------------------------- Tabs */

/*
    The two halves of one visit: the form, and the workspace that gathers its evidence.

    A strip rather than a menu, because there are two of them and an evaluator moves
    between them repeatedly during a single lesson.
*/
/*
    A bordered strip holding both tabs, the current one filled.

    inline-flex, not flex: the strip is as wide as its tabs. Stretched across the
    page it stops reading as a pair of choices and starts reading as a header.
*/
/*
    A bordered strip holding both tabs: the strip is recessed, the current tab is
    raised out of it in the page's own surface colour, the other is muted.

    That relationship — not a particular colour — is what makes the light strip and
    the one on the workspace's dark bar read as the same control. Filling the
    current tab with the accent colour broke it: same shape, different thing.

    inline-flex, not flex: the strip is as wide as its tabs. Stretched across the
    page it stops reading as a pair of choices and starts reading as a header.
*/
/*
    The strip and anything standing next to it are one height.

    Sync is a small button and the tab strip is a bordered container of small buttons,
    so left to themselves they came out a few pixels apart — close enough to read as a
    mistake rather than a distinction.
*/
.fs-tabs,
.fs-tabs__beside { min-height: 2.6rem; }

.fs-tabs__beside {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
}

.fs-tabs {
    display: inline-flex;
    align-items: center;
    gap: .15rem;
    padding: .15rem;
    border: 1px solid var(--fs-line);
    border-radius: var(--fs-radius-sm);
    background: var(--fs-canvas);
}

/*
    Both tabs the same width, because they are the two halves of one visit and neither
    is the larger idea. Sized off the longer label rather than the content of each, so
    the strip does not change shape when the wording does.
*/
.fs-tabs__tab {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 1 1 0;
    min-width: 11rem;
    gap: .45rem;
    padding: .4rem .8rem;
    border: 1px solid transparent;
    border-radius: var(--fs-radius-sm);
    background: none;
    color: var(--fs-muted);
    font-size: .88rem;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
}

.fs-tabs__tab:hover { background: var(--fs-line-soft); color: var(--fs-body); }

.fs-tabs__tab--on {
    background: var(--fs-surface);

    /* The border matches the fill rather than outlining it. A grey ring here and
       none on the dark strip made one an outlined pill and the other a plain one —
       the same shape reading as two different controls. The shadow does the lifting. */
    border-color: var(--fs-surface);
    color: var(--fs-ink);
    box-shadow: 0 1px 2px rgba(11, 18, 32, .12);
    cursor: default;
}

.fs-tabs__tab--on:hover { background: var(--fs-surface); color: var(--fs-ink); }

.fs-tabs__tab--on:hover { background: var(--fs-blue); color: #fff; }

/* Not yet a place to go — a thing to start. Reads as an invitation, not a location. */
.fs-tabs__tab--action { border-style: dashed; border-color: var(--fs-line-strong); }

/* Evidence carried across from the workspace, marked so its origin stays visible
   inside a field the evaluator can also type into. */
.fs-from-observation {
    border-left: 2px solid var(--fs-blue);
    padding-left: .6rem;
    margin-bottom: .4rem;
}

/*
    The same strip on the workspace's dark bar.

    Only the colours change: the shape an evaluator learned on the form is the shape
    they meet here, which is the whole point of using one control in both places.
*/
.fs-tabs--dark {
    border-color: rgba(255, 255, 255, .22);
    background: rgba(0, 0, 0, .18);
}

.fs-tabs--dark .fs-tabs__tab { color: rgba(255, 255, 255, .75); }
.fs-tabs--dark .fs-tabs__tab:hover { background: rgba(255, 255, 255, .12); color: #fff; }

.fs-tabs--dark .fs-tabs__tab--on {
    background: #fff;
    border-color: #fff;
    color: var(--fs-ink);
}

/* The shared visit header. On a full-bleed page it needs the page gutters the rest
   of the application gets from its container. */
/*
    The title block carried its own left indent from when this page ran to the edge of
    the window and nothing else provided one. The page now sits in the same column as
    the rest of the application, which supplies the gutter — so keeping the indent as
    well pushed the heading and its tabs further in than the identical heading on the
    evaluation form, one tab away. Vertical spacing is matched to the shell's own.
*/
.fs-visit-head--full {
    padding: clamp(1rem, 1.6vw, 2rem) 0 .75rem;
    background: var(--fs-canvas);
}

/*
    The same space under the header on the form.

    The live-notes page is full-bleed, so it supplies its own padding above and below
    the header; the form gets the padding above from the main column and had nothing
    below. The two halves therefore stood a few pixels apart, and switching tabs
    nudged the date row and every section under it.
*/
.fs-visit-head--form {
    padding-bottom: .75rem;

    /*
        The whole width of the row it sits in.

        It is the only child of a flex row, so it shrank to its content while the
        live-notes half — which has no such row — spanned everything. The titles
        lined up and the buttons did not: they ended 460 pixels apart on a 1600px
        screen, so switching tabs threw them across the page. Measured, not guessed.
    */
    flex: 1 1 100%;
    width: 100%;
}

/*
    What the visit was — when, which grade, which course — in a row under the tabs.

    The same row on both halves of the visit, so switching tabs does not rearrange the
    page: controls on the form, plain captions on the live notes, laid out identically
    either way.
*/
.fs-visitfacts {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: .6rem 1rem;
    margin-top: .85rem;
}

/*
    One fixed size, not a share of what is left.

    Sharing the row meant the controls measured whatever container they were in — and
    the two halves of a visit have different containers, so the same three fields came
    out one width on the evaluation form and another on the live notes. Switching tabs
    resized them. A fixed basis is the same on both, and still wraps when the row runs
    out of room.
*/
.fs-visitfacts__item { flex: 0 0 17rem; max-width: 100%; }

.fs-visitfacts .form-label {
    display: block;
    margin: 0 0 .2rem;
    font-size: .78rem;
    font-weight: 600;
    color: var(--fs-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* One height between them: a select and a date input do not agree by default. */
.fs-visitfacts .form-control,
.fs-visitfacts .form-select {
    width: 100%;
    height: 2.6rem;
    padding-block: 0;
}

.fs-visitfacts__state { flex: 0 0 auto; align-self: center; }

/* The visit's own tools, at the end of the visit's own row. */
.fs-visitfacts__tools {
    display: flex;
    gap: .5rem;
    flex-wrap: wrap;
    margin-left: auto;
    align-self: flex-end;
}

.fs-visitfacts__tools .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .4rem;
    min-height: 2.6rem;
}

@media (max-width: 575.98px) {
    .fs-visitfacts__tools { margin-left: 0; width: 100%; }
}

@media (max-width: 575.98px) {
    .fs-visitfacts__item { min-width: 0; max-width: none; flex: 1 1 100%; }
}

/*
    On a phone the chrome above the writing had left the evidence stream 24px of an
    844px screen — the notes an evaluator has already taken were effectively not on
    the page. The title block is the part that can afford to give: it names a room
    the reader is standing in.
*/
@media (max-width: 575.98px) {
    .fs-visit-head--full { padding: .6rem 0 .5rem; }

    .fs-visit-head h1 {
        font-size: 1.15rem;
        line-height: 1.25;
        margin-bottom: .15rem;
    }

    .fs-visit-head .d-flex { margin-top: .6rem !important; }
}

/* The visit bar spans the window on both halves of a visit because the layout renders
   it above <main> rather than inside it — see the VisitBar section in _Layout. */

/* The row whose detail is open below the table, so the two read as one thing. */
.fs-row--open > td { background: var(--fs-blue-soft); }

/* A row's actions: small icon buttons that stay on one line and do not stretch
   the column they sit in. */
.fs-rowactions {
    display: inline-flex;
    align-items: center;
    gap: .2rem;
    flex-wrap: nowrap;
}

.fs-rowactions form { margin: 0; }
.fs-rowactions .disabled { opacity: .4; cursor: not-allowed; }
.fs-danger-hover:hover { color: var(--fs-red); border-color: var(--fs-red); }

/* =====================================================================
   A text box with its own suggestion list.

   Replaces <datalist>, which the browser draws in the operating system's
   colours, unstyleable, and tall enough with two hundred entries to
   cover the form underneath it. See combo.js.
   ===================================================================== */

.fs-combo { position: relative; }

.fs-combo__source { display: none; }

.fs-combo__list {
    position: absolute;
    top: calc(100% + 2px);
    left: 0;
    right: 0;
    z-index: 1050;
    margin: 0;
    padding: .25rem;
    list-style: none;
    max-height: 16rem;
    overflow-y: auto;
    background: var(--fs-surface);
    border: 1px solid var(--fs-line-strong);
    border-radius: var(--fs-radius-sm);
    box-shadow: 0 10px 28px rgba(15, 23, 41, .16);
}

.fs-combo__option {
    padding: .35rem .55rem;
    border-radius: var(--fs-radius-sm);
    font-size: .875rem;
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.fs-combo__option:hover,
.fs-combo__option.is-active { background: var(--fs-blue-soft); color: var(--fs-blue-dark); }

/* What the cap is hiding, so a narrowed list never looks like the whole list. */
.fs-combo__more {
    padding: .35rem .55rem;
    font-size: .78rem;
    color: var(--fs-muted);
    border-top: 1px solid var(--fs-line-soft);
    margin-top: .2rem;
}

/* --- Permissions grid ------------------------------------------------- */

/* The grid can be wider than the page on a small screen. It scrolls inside its own
   box rather than pushing the page sideways. */
.fs-matrix__scroll {
    overflow: auto;
    max-height: calc(100vh - var(--fs-header-h, 4.5rem) - 12rem);
}

.fs-matrix {
    border-collapse: separate;
    border-spacing: 0;
    width: 100%;
    font-size: .9rem;
}

/* Both headers stay put. Scanning "can a School Admin do this?" twenty rows down is
   guesswork otherwise — the column heading has long since scrolled away. */
.fs-matrix thead th {
    position: sticky;
    top: 0;
    z-index: 3;
    background: var(--fs-surface, #fff);
    border-bottom: 2px solid var(--fs-border, #e2dddb);
    padding: .6rem .5rem;
    text-align: center;
    vertical-align: bottom;
}

.fs-matrix__rowhead {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--fs-surface, #fff);
    border-right: 1px solid var(--fs-border, #e2dddb);
    text-align: left;
    font-weight: 500;
    padding: .5rem .75rem;
    min-width: 20rem;
    max-width: 26rem;
}

/* The corner sits above both. */
.fs-matrix thead .fs-matrix__rowhead { z-index: 4; }

.fs-matrix__group { font-weight: 600; white-space: nowrap; }
.fs-matrix__col { min-width: 7.5rem; }
.fs-matrix__all { padding: 0; font-size: .75rem; text-decoration: none; }

.fs-matrix__name { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }

.fs-matrix__what {
    font-size: .78rem;
    line-height: 1.3;
    margin-top: .1rem;
}

.fs-matrix__area th {
    position: sticky;
    left: 0;
    background: var(--fs-navy-soft, #0a2a5c);
    color: #eaf0f8;
    font-size: .72rem;
    text-transform: uppercase;
    letter-spacing: .06em;
    padding: .35rem .75rem;
}

.fs-matrix tbody tr:not(.fs-matrix__area):hover .fs-matrix__rowhead,
.fs-matrix tbody tr:not(.fs-matrix__area):hover .fs-matrix__cell {
    background: var(--fs-hover, #f5f2f1);
}

.fs-matrix__cell {
    text-align: center;
    padding: .5rem;
    border-bottom: 1px solid var(--fs-border-soft, #f1eeed);
    background: var(--fs-surface, #fff);
}

.fs-matrix__cell .form-check-input { margin: 0; cursor: pointer; }

/* An edited square, so a stray click is visible before it is saved. */
.fs-matrix__cell.is-changed {
    background: #fff4d6;
    box-shadow: inset 0 0 0 2px #e0a800;
}

/* --- The save bar ------------------------------------------------------ */

.fs-savebar {
    position: sticky;
    top: var(--fs-header-h, 4.5rem);
    z-index: 5;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .75rem;
    background: var(--fs-navy-soft, #0a2a5c);
    color: #eaf0f8;
    border-radius: var(--fs-radius, .5rem);
    padding: .6rem .85rem;
    margin-bottom: .75rem;
    box-shadow: var(--fs-shadow-lg, 0 6px 18px rgba(15, 23, 41, .2));
}

.fs-savebar__actions { display: flex; gap: .4rem; margin-left: auto; }

/* --- Icon-only row actions -------------------------------------------- */

.fs-rowicons {
    display: inline-flex;
    gap: .25rem;
    justify-content: flex-end;
}

/* Square, so three of them read as a set of controls rather than three words of
   different lengths. */
.fs-iconbtn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2rem;
    padding-inline: .45rem;
}

/* --- Ad-hoc walkthrough person buttons --------------------------------- */

/* The person is a button rather than a link for a coach: choosing somebody starts
   the visit. It has to sit in the same grid as the links beside it. */
.fs-person__form { display: contents; }

.fs-person--start {
    width: 100%;
    text-align: left;
    border: 1px solid var(--fs-line, #e2dddb);
    background: #fff;
    cursor: pointer;
}

.fs-person__go {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    margin-left: auto;
    font-size: .8rem;
    font-weight: 600;
    color: var(--fs-blue, #001842);
}

.fs-person--start:hover { border-color: var(--fs-blue, #001842); }

/* --- Ad-hoc walkthroughs ----------------------------------------------- */

.fs-adhoc__filters {
    display: grid;
    gap: .75rem;
    align-items: end;
    grid-template-columns: minmax(12rem, 2fr) repeat(2, minmax(9rem, 1fr)) auto;
}

.fs-adhoc__scroll { overflow-x: auto; }

.fs-adhoc td form { margin: 0; }

/*
    Below this the table stops being a table.

    Four columns and an action do not fit a phone, and a sideways scroll on a list you
    are meant to skim is worse than no table at all — so each row becomes its own
    block with the column name beside the value. The names come from data-label on
    each cell, which the markup already carries.
*/
@media (max-width: 767.98px) {
    .fs-adhoc__filters { grid-template-columns: 1fr; }

    .fs-adhoc thead { display: none; }

    .fs-adhoc,
    .fs-adhoc tbody,
    .fs-adhoc tr,
    .fs-adhoc td { display: block; width: 100%; }

    .fs-adhoc tr {
        position: relative;
        border: 1px solid var(--fs-line, #e2dddb);
        border-radius: var(--fs-radius, .5rem);
        padding: .6rem .75rem;
        margin-bottom: .5rem;
    }

    .fs-adhoc td {
        border: none;
        padding: .15rem 0;
        /* Room for the start button, which sits in the corner. */
        padding-right: 3rem;
    }

    /* The column name, so a value is never stranded without its heading. */
    .fs-adhoc td[data-label]::before {
        content: attr(data-label) ": ";
        font-weight: 600;
        color: var(--fs-muted, #5a6270);
    }

    /* A blank cell would otherwise show a label with nothing after it. */
    .fs-adhoc td[data-label]:empty { display: none; }

    .fs-adhoc td:last-child {
        position: absolute;
        top: .6rem;
        right: .75rem;
        padding: 0;
        width: auto;
    }
}

/* --- A component section's verdict, beside its heading --------------------- */

.fs-card__head--verdict {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
}

/* The choice keeps its buttons but loses the stacked spacing a field carries
   when it sits in the body of a card. */
.fp-verdict .mb-3 { margin-bottom: 0 !important; }
.fp-verdict .fs-choice { margin: 0; }
.fp-verdict .fs-choice__button { white-space: nowrap; }

@media (max-width: 575.98px) {
    .fs-card__head--verdict { align-items: flex-start; }
    .fp-verdict { width: 100%; }
}

/*
    A teacher's year, read from inside a visit.

    Laid out as rows rather than a table: the panel is scanned for one line — the visit
    somebody half remembers — and a table invites reading every column of every row.
*/
/*
    The title block takes the width it is given, and the Forms button goes to the end.

    On the Full Form the block stands beside History and Print inside a flex row, so it
    sized itself to its own contents — and the button, pushed to the end of that box,
    landed in the middle of the page. On Quick Evidence, where the block spans on its
    own, the same button sat at the edge. One header, two positions.
*/
.fs-visit-head { flex: 1 1 auto; min-width: 0; }

/*
    The title on the left, the three ways of looking at the record on the right.
*/
.fs-visit-head__top {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1.5rem;
    flex-wrap: wrap;
}

.fs-visit-head__name { min-width: 0; }

/*
    One size between them.

    Three buttons whose words happen to be different lengths came out three widths,
    which reads as three unrelated controls rather than one set. Sized off the longest
    of them and centred, so the group stays square when the wording changes.
*/
.fs-visit-actions {
    display: flex;
    gap: .5rem;
    flex-wrap: wrap;
}

.fs-visit-actions .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .4rem;
    min-width: 7.5rem;
    min-height: 2.6rem;
}

.fs-visit-head__aside { margin-left: auto; }

/* The heading over a set of requirements: Walkthroughs, Observations, Meetings. */
.vh-path {
    display: flex;
    align-items: center;
    gap: .5rem;
    font-weight: 700;
    font-size: 1rem;
    margin: 1.25rem 0 .6rem;
    padding-bottom: .3rem;
    border-bottom: 2px solid var(--fs-line);
}

.vh-path:first-child { margin-top: 0; }
.vh-group { margin-bottom: 1.25rem; }

.vh-group__head {
    display: flex;
    align-items: center;
    gap: .5rem;
    font-size: .85rem;
    font-weight: 600;
    color: var(--fs-muted);
    margin: 0 0 .35rem;
}

.vh-list { list-style: none; margin: 0; padding: 0; }

.vh-row {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .45rem .1rem;
    border-top: 1px solid var(--fs-line-soft);
}

.vh-row__n { flex: 0 0 2.2rem; font-size: .8rem; color: var(--fs-muted); }

/* The same column, holding a name instead of an occurrence number. */
.vh-row__n--name {
    flex: 0 0 auto;
    min-width: 12rem;
    font-size: .9rem;
    font-weight: 600;
    color: var(--fs-body);
}

.vh-row__what {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .4rem;
    flex: 1 1 auto;
    min-width: 0;
}

.vh-row__go { display: flex; gap: .35rem; flex: 0 0 auto; }

@media (max-width: 575.98px) {
    .fs-visit-head__aside { margin-left: 0; }
    .vh-row { flex-wrap: wrap; }
}

/*
    A form read inside the panel.

    It arrives wearing the printed record's own classes, which assume a page of their
    own — so the page padding is taken back off and the width let go.
*/
.vh-form.print-body { padding: 0; max-width: none; margin: 0; font-size: .95rem; }

/* A suggestion read beside the component it is about. */
.fp-score { margin-top: .75rem; max-width: 46rem; }

/*
    The visit in numbers, in one row above the feedback.

    The tiles share the row rather than each taking a fixed width, so the block ends
    where every other card on the page ends instead of stopping short with a gap after
    it. They wrap onto a second line only when they can no longer be read at that
    width — which on a phone is immediately.
*/
.fp-tiles {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
    margin-bottom: 1rem;
}

/* One width between them, so the row reads as a set of readings of one visit
   rather than as tiles that happen to sit next to each other. */
.fp-tile {
    flex: 1 1 6rem;
    min-width: 0;
    padding: .4rem .5rem;
    text-align: center;
    background: var(--fs-surface);
    border: 1px solid var(--fs-line);
    border-radius: var(--fs-radius-sm);
}

.fp-tile__value { font-size: 1.15rem; font-weight: 700; line-height: 1.1; }

/* The code, at a size worth reading. It is what an evaluator scans the row for, and
   the component's full name is on hover for the moment it is not obvious. */
.fp-tile__label {
    font-size: .8rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--fs-body);
    margin-top: .05rem;
    line-height: 1.2;
}



/* The average is a real answer on the form, so it keeps its input — only smaller,
   and without the sentence explaining how a mean works. */
.fp-tile--calc .form-control {
    border: none;
    background: none;
    text-align: center;
    font-size: 1.15rem;
    font-weight: 700;
    padding: 0;
    height: auto;
}

.fp-tile--calc .fs-hint, .fp-tile--calc .mb-3 { display: none; }
.fp-tile--calc > div:first-child { margin-bottom: 0 !important; }

.fp-tile--total { border-color: var(--fs-blue); }

/* Reports: a date that has passed, and a row nobody has been to see. */
.fs-overdue { color: var(--fs-danger, #b42318); font-weight: 600; }
.fs-row--none td { background: #fffaf5; }
.fs-row--total td { border-top: 2px solid var(--fs-line); }

/* One bar per month, drawn in the table. Nothing to load, nothing to go wrong. */
.rp-bar {
    height: .6rem;
    background: var(--fs-line-soft);
    border-radius: 999px;
    overflow: hidden;
}

.rp-bar span {
    display: block;
    height: 100%;
    background: var(--fs-blue);
    border-radius: 999px;
}


/*
    Reports: goals, and the year's pace.

    Everything here is drawn with CSS against numbers the server already worked out.
    No chart library, which keeps the strict content-security policy intact and means a
    report that renders at all renders completely — there is no second request that can
    fail and leave an empty box where the picture should be.
*/

.rp-rates strong { color: var(--fs-body); }

/* One bar per administrator, all against the same full-width goal. */
.rp-chart { display: grid; gap: .4rem; }

.rp-chart__row {
    display: grid;
    grid-template-columns: minmax(7rem, 14rem) 1fr 3.25rem;
    align-items: center;
    gap: .75rem;
}

.rp-chart__name {
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.rp-chart__value { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }

.rp-goal {
    height: .85rem;
    background: var(--fs-line-soft);
    border-radius: 999px;
    overflow: hidden;
}

.rp-goal span { display: block; height: 100%; border-radius: 999px; background: var(--fs-blue); }

/*
    Four readings, not a gradient. An administrator scanning this needs to know which
    of four things is true about a number, and a shade of orange does not say that.
*/
.rp-goal--met span { background: var(--fs-green); }
.rp-goal--near span { background: var(--fs-blue); }
.rp-goal--part span { background: var(--fs-amber); }
.rp-goal--behind span { background: var(--fs-red); }

/* The same four readings as a percentage in a table cell. */
.rp-pct {
    display: inline-block;
    min-width: 3.25rem;
    padding: .05rem .35rem;
    border-radius: var(--fs-radius-sm);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}

.rp-pct.rp-goal--met { background: var(--fs-green-soft); color: var(--fs-green); }
.rp-pct.rp-goal--near { background: var(--fs-blue-soft); color: var(--fs-blue-dark); }
.rp-pct.rp-goal--part { background: var(--fs-amber-soft); color: var(--fs-amber); }
.rp-pct.rp-goal--behind { background: var(--fs-red-soft); color: var(--fs-red); }

/* Core and non-core are four separate questions; the header says where each begins. */
.rp-goals .rp-group {
    text-align: center;
    border-left: 1px solid var(--fs-line);
    font-size: .8rem;
}

.rp-goals td, .rp-goals th { white-space: nowrap; }
.rp-goals td:nth-child(2), .rp-goals td:nth-child(5),
.rp-goals td:nth-child(8), .rp-goals td:nth-child(11) { border-left: 1px solid var(--fs-line-soft); }

/* ------------------------------------------------------------------- quarters */

.rp-legend { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: .75rem; font-size: .85rem; }
.rp-legend__item { display: inline-flex; align-items: center; gap: .35rem; }

.rp-swatch { width: .8rem; height: .8rem; border-radius: 3px; display: inline-block; }

.rp-q1 { background: var(--fs-blue); }
.rp-q2 { background: var(--fs-teal); }
.rp-q3 { background: var(--fs-violet); }
.rp-q4 { background: var(--fs-green); }

.rp-columns {
    display: flex;
    align-items: flex-end;
    gap: 1.25rem;
    overflow-x: auto;
    padding-bottom: .25rem;
}

.rp-columns__group { flex: 0 0 auto; min-width: 5.5rem; }

/*
    A fixed plot height so every group is measured the same way, and the columns grow
    from the floor of it rather than from wherever their own text ended.
*/
.rp-columns__plot {
    display: flex;
    align-items: flex-end;
    gap: .25rem;
    height: 9rem;
    border-bottom: 1px solid var(--fs-line);
    padding-bottom: 1px;
}

.rp-col {
    flex: 1 1 0;
    min-height: 2px;
    border-radius: 3px 3px 0 0;
    position: relative;
}

/* The count sits above its column, which keeps a short column readable. */
.rp-col__value {
    position: absolute;
    top: -1.15rem;
    left: 0;
    right: 0;
    text-align: center;
    font-size: .75rem;
    font-weight: 600;
    color: var(--fs-muted);
}

.rp-columns__name {
    margin-top: .4rem;
    font-size: .8rem;
    text-align: center;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* One row's whole year as a single bar: pace, rather than volume. */
.rp-spread {
    display: flex;
    height: .6rem;
    border-radius: 999px;
    overflow: hidden;
    background: var(--fs-line-soft);
}

.rp-spread span { display: block; height: 100%; }


/*
    The other reports, drawn.

    Same idea as the goal bars above: the server has already worked the number out, so
    the picture is a width. What changes between reports is only what the colours mean.
*/

/* Two kinds of visit in one bar, both measured against the busiest evaluator. */
.rp-stack { display: flex; height: .85rem; border-radius: 999px; overflow: hidden; background: var(--fs-line-soft); }
.rp-stack span { display: block; height: 100%; }

.rp-wt { background: var(--fs-blue); }
.rp-fo { background: var(--fs-violet); }
.rp-ev { background: var(--fs-teal); }
.rp-slow { background: var(--fs-amber); }

/*
    Performance levels, lowest to highest. Deliberately not a red-to-green scale of
    approval: unsatisfactory is the level that needs support soonest, which is what the
    warm end marks, and distinguished is not a reward colour.
*/
.rp-l1 { background: var(--fs-red); }
.rp-l2 { background: var(--fs-amber); }
.rp-l3 { background: var(--fs-blue); }
.rp-l4 { background: var(--fs-green); }
.rp-l5 { background: var(--fs-violet); }

/* A component nothing has been filed against, in the chart as well as the table. */
.rp-chart__row--none .rp-chart__name { color: var(--fs-red); }

/* No goal to measure against: a track with nothing in it, not a bar at zero. */
.rp-goal--none span { background: none; }
.rp-pct.rp-goal--none { color: var(--fs-muted); }


/*
    The year as a grid.

    Five steps of shading rather than a continuous scale, because this is read by
    scanning for the pale columns and a person cannot tell 46% from 52% by colour. The
    numbers are in the cells for anyone who needs the actual figure.
*/
.rp-grid { font-size: .85rem; }

.rp-grid__step {
    text-align: center;
    font-size: .75rem;
    font-weight: 600;
    min-width: 4.5rem;
    max-width: 7rem;
    white-space: normal;
    line-height: 1.15;
}

/* The building name stays put while the year scrolls past it. */
.rp-grid__corner {
    position: sticky;
    left: 0;
    background: var(--fs-surface);
    z-index: 1;
    white-space: nowrap;
}

.fs-row--total .rp-grid__corner { background: var(--fs-canvas); }

.rp-heat {
    text-align: center;
    font-variant-numeric: tabular-nums;
    border-left: 1px solid var(--fs-surface);
}

.rp-heat--0 { background: var(--fs-red-soft); color: var(--fs-red); }
.rp-heat--1 { background: var(--fs-amber-soft); color: var(--fs-amber); }
.rp-heat--2 { background: var(--fs-blue-soft); color: var(--fs-blue-dark); }
.rp-heat--3 { background: #d7dfeb; color: var(--fs-blue-dark); }
.rp-heat--4 { background: var(--fs-green-soft); color: var(--fs-green); }
.rp-heat--na { background: var(--fs-canvas); }

/* The same five readings as a bar, for the column summary underneath. */
.rp-goal.rp-heat--0, .rp-goal.rp-heat--1, .rp-goal.rp-heat--2,
.rp-goal.rp-heat--3, .rp-goal.rp-heat--4, .rp-goal.rp-heat--na {
    background: var(--fs-line-soft);
}

.rp-goal.rp-heat--0 span { background: var(--fs-red); }
.rp-goal.rp-heat--1 span { background: var(--fs-amber); }
.rp-goal.rp-heat--2 span { background: var(--fs-blue); }
.rp-goal.rp-heat--3 span { background: var(--fs-blue-dark); }
.rp-goal.rp-heat--4 span { background: var(--fs-green); }
.rp-goal.rp-heat--na span { background: none; }

/* The legend swatches need a visible edge; two of the six are nearly white. */
.rp-legend .rp-swatch { border: 1px solid var(--fs-line); }


/*
    Pace against the year's own curve.

    A mark on the bar rather than a second bar beside it: the question is whether the
    filled part has reached the line, and two bars would make a reader compare lengths
    instead of just looking.
*/
.rp-goal { position: relative; }

.rp-goal__mark {
    position: absolute;
    top: -2px;
    bottom: -2px;
    width: 2px;
    margin-left: -1px;
    background: var(--fs-ink);
    opacity: .55;
    border-radius: 1px;
}

/* Met the milestone. Green, and the only green on this page that means "nothing to do". */
.fs-pill--ok { background: var(--fs-green-soft); color: var(--fs-green); }

/* The milestone mark, in a legend where it has no bar to sit on. */
.rp-mark { background: var(--fs-ink); opacity: .55; }

/* Two milestones for one quarter, stacked so each is read against the whole year. */
.rp-pair { display: grid; gap: .3rem; }
.rp-pair .rp-goal { height: .55rem; }

/* Each bar carries its own number, on the same line, so neither has to be matched
   to the other by position. */
.rp-pair__line {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 3rem;
    align-items: center;
    gap: .75rem;
}

.rp-pair__value {
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    font-size: .85rem;
    white-space: nowrap;
}

/*
    The curve chart has no third column: its numbers live inside the pair.

    Scoped rather than changed on .rp-chart__row, which eight report views share and
    which is right for them - they carry one bar and one number per row.
*/
.rp-curve { gap: .75rem; }
.rp-curve .rp-chart__row { grid-template-columns: 2.5rem 1fr; gap: .75rem; }

/*
    The fill colours the legend promises.

    ".rp-goal span" sets the blue and, being a class plus an element, outranks a bare
    .rp-fo - so the formal-observation bar drew blue while the legend showed violet.
    Matching that weight is what makes the legend true.
*/
.rp-goal span.rp-wt { background: var(--fs-blue); }
.rp-goal span.rp-fo { background: var(--fs-violet); }


/*
    The assistant's mark.

    Square by construction rather than by two numbers kept in agreement: the height is
    stated, the width follows it. Setting a width of its own was the mistake the first
    time — it sat under the touch floor, so the height won and the width did not, and
    the button came out a rounded oblong.

    Padding is removed rather than balanced. With the box already square and the icon
    centred in it, padding only fights the size the button has been given.
*/
.fs-ai {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    align-self: center;
    aspect-ratio: 1;
    padding: 0;
    flex: 0 0 auto;
    height: 2.25rem;
    min-height: 2.25rem;
    min-width: 2.25rem;
}

/*
    Back to a full target where a finger is doing the pointing.

    36px is comfortable with a mouse and too small on a tablet, which is where half of
    these visits are actually written up. The button is only shrunk for pointers that
    can be precise about it.
*/
@media (pointer: coarse) {
    .fs-ai { height: 2.75rem; min-height: 2.75rem; min-width: 2.75rem; }
}

.fs-ai .bi { font-size: 1.05rem; line-height: 1; }

/* Enough to read as "this one is the machine" without competing with the form. */
.fs-ai:not(:disabled) { color: var(--fs-violet); }
.fs-ai:not(:disabled):hover { color: var(--fs-violet); border-color: var(--fs-violet); background: var(--fs-violet-soft); }


/*
    The multiple-choice filters on the ad-hoc list.

    A native multiple select ignores the height a single one is given and sizes itself
    to its rows, so left alone it towers over the search box beside it. Bounded here
    instead, and allowed to scroll — a district with eleven campuses should not push the
    table off the screen before anybody has filtered anything.
*/
.fs-multi {
    height: auto;
    min-height: 2.25rem;
    max-height: 7.5rem;
    overflow-y: auto;
    padding: .15rem;
}

.fs-multi option { padding: .15rem .35rem; border-radius: 4px; }

/* The browser default for a selected row is a flat highlight that reads as disabled. */
.fs-multi option:checked {
    background: var(--fs-blue-soft);
    color: var(--fs-blue-dark);
    font-weight: 600;
}


/*
    The ad-hoc list, spaced like the staff list.

    It used to carry the dense modifier, which is right for a table somebody scans for a
    value and wrong for one they read a name off and then click. The two lists are read
    the same way, so they are spaced the same way.
*/
.fs-adhoc thead th { white-space: nowrap; }

/* The hint sits under both pickers, not in a column of its own. */
.fs-adhoc__hint { grid-column: 1 / -1; }


/*
    The tick-list filters, and the headings that sort.

    A closed picker has to say what it is holding back, so the button carries a summary
    and takes a mark of its own once anything is ticked. A filter that looks identical
    switched on and switched off is how somebody spends ten minutes wondering where the
    rest of the district went.
*/
.fs-tick { position: relative; }

.fs-tick__toggle--set { border-color: var(--fs-blue); font-weight: 600; }

.fs-tick__menu {
    max-height: 16rem;
    overflow-y: auto;
    padding: .35rem;
    min-width: 100%;
}

.fs-tick__item {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .35rem .5rem;
    border-radius: var(--fs-radius-sm);
    cursor: pointer;
    white-space: nowrap;
}

.fs-tick__item:hover { background: var(--fs-line-soft); }
.fs-tick__item input { margin: 0; flex: 0 0 auto; }

/*
    The heading is the control.

    A button rather than a click handler on the cell, so it can be tabbed to and pressed
    with a keyboard — and stripped back to look like the heading it replaced, because a
    row of buttons across the top of a table reads as a toolbar.
*/
.fs-sort {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    color: inherit;
    text-transform: inherit;
    letter-spacing: inherit;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: .3rem;
}

.fs-sort:hover { color: var(--fs-blue-dark); }

/* The arrow appears on the column actually in use, and points the way it is sorted. */
.fs-sort::after {
    content: "";
    width: .4rem;
    height: .4rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg) translate(-1px, -1px);
    opacity: 0;
}

th[aria-sort="ascending"] .fs-sort::after { opacity: .7; transform: rotate(-135deg) translate(-2px, -2px); }
th[aria-sort="descending"] .fs-sort::after { opacity: .7; }
.fs-sort:hover::after { opacity: .35; }


/*
    The evidence filters, on one line.

    They were a twelve-column Bootstrap grid holding nineteen columns' worth of
    controls, so they wrapped — and the two that appear only sometimes (submitted by,
    school) moved the rest around as they came and went. A flex row sizes to whatever is
    actually on the page and stays put.
*/
.ev-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: .75rem;
}

/* Every picker shares the room; min-width lets them shrink rather than overflow. */
.ev-filters > div { flex: 1 1 9rem; min-width: 0; }

/* Names, components and free text are the long ones. */
.ev-filters__wide { flex: 1 1 15rem !important; }

/*
    The two switches and the button, kept together at the end.

    Fixed rather than sharing the stretch: a Search button that grows to fill whatever
    is left over is the widest thing on the row on a large screen, which is not what it
    deserves to be.
*/
.ev-filters__actions {
    flex: 0 0 auto !important;
    display: flex;
    align-items: flex-end;
    gap: .9rem;
}

/*
    Side by side, not stacked.

    Stacked, the pair stood half a control taller than everything beside it, and since
    the row bottom-aligns, the extra height went upward — the switches ended up
    hovering above the line they belong to.
*/
.ev-filters__actions .form-check { margin: 0 0 .35rem 0; white-space: nowrap; }
.ev-filters__actions .btn { width: auto; }

/* Whose lesson the evidence is about, in a bank that spans the district. */
.ev-entry__staff { font-weight: 600; color: var(--fs-blue-dark); text-decoration: none; }
.ev-entry__staff:hover { text-decoration: underline; }


/*
    The quality checks.

    The count leads the row rather than trailing it. An administrator scanning this page
    is looking for the numbers that are not nought, and a figure in the last column is
    found only after reading the label of every check above it.
*/
.qc-count {
    display: inline-block;
    min-width: 2.75rem;
    padding: .25rem .5rem;
    border-radius: var(--fs-radius-sm);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.qc-count--open { background: var(--fs-amber-soft); color: var(--fs-amber); }

/* Nought is a result, not an absence — it earns a colour of its own. */
.qc-count--clear { background: var(--fs-green-soft); color: var(--fs-green); }

/* What to do about it, set apart from what it is. */
.qc-todo {
    margin-top: .35rem;
    padding-left: .6rem;
    border-left: 2px solid var(--fs-blue);
    font-size: .85rem;
    color: var(--fs-body);
}


/*
    The quality checks as a row of tiles.

    Equal width and wrapping, so the row reads as one set of readings rather than as
    tiles that happen to sit together — and so adding a tenth check does not force a
    decision about which line it belongs on.
*/
.qc-tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: .75rem;
}

.qc-tile {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: .5rem;
    min-height: 6.5rem;
    padding: 1.1rem 1.15rem;
    border: 1px solid var(--fs-line);
    border-radius: var(--fs-radius-sm);
    background: var(--fs-surface);
    text-decoration: none;
    color: inherit;
    transition: border-color .1s ease, background-color .1s ease;
}

/* Only a tile with something behind it is a link; the rest are readings. */
a.qc-tile[href]:hover { border-color: var(--fs-blue); background: var(--fs-blue-soft); }
.qc-tile:not([href]) { cursor: default; }

.qc-tile__value { font-size: 2.4rem; font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; }

.qc-tile__label {
    font-size: .85rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--fs-muted);
    line-height: 1.2;
}

/* Nought is a result. Amber is a prompt, not an alarm — none of these are failures. */
.qc-tile--open { border-color: var(--fs-amber); }
.qc-tile--open .qc-tile__value { color: var(--fs-amber); }
.qc-tile--clear .qc-tile__value { color: var(--fs-green); }

/* The tile whose records are showing. */
.qc-tile--showing { border-color: var(--fs-blue); background: var(--fs-blue-soft); box-shadow: inset 0 -3px 0 var(--fs-blue); }

/* ---------------------------------------------------- Evaluation path editor */

.ep-path { border-top: 1px solid var(--fs-line-soft); }

/* One step. The number and the move buttons sit in a fixed rail so the names on
   every row start at the same place, grouped or not. */
.ep-step {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: .75rem;
    padding: .7rem 1rem;
    border-bottom: 1px solid var(--fs-line-soft);
}

.ep-step__body { flex: 1 1 12rem; min-width: 0; }

/* Edit, copy, remove as one set. */
.ep-step__tools { flex: none; display: flex; gap: .25rem; }
.ep-step__tools form { display: contents; }

.ep-step__danger:hover { border-color: var(--fs-red); color: var(--fs-red); }

.ep-step--optional { background: var(--fs-canvas); }

/* Fixed width, so every row's name starts in the same place whether its number is
   one digit or three. */
.ep-step__bar {
    flex: none;
    width: 3.25rem;
    display: flex;
    align-items: center;
    gap: .35rem;
    padding-top: .1rem;
}

/* What a drag starts from. A button, so it takes keyboard focus — up and down move
   the step without a pointer. */
.ep-step__grip {
    border: none;
    background: none;
    padding: 0 .1rem;
    color: var(--fs-line);
    cursor: grab;
    line-height: 1;
}

.ep-step__grip:hover { color: var(--fs-muted); }
.ep-step__grip:active { cursor: grabbing; }

.ep-step__grip:focus-visible {
    outline: 2px solid var(--fs-blue);
    outline-offset: 2px;
    border-radius: var(--fs-radius-sm);
    color: var(--fs-blue);
}

/* The row being carried. Faded rather than hidden, so the gap it will leave stays
   visible while the rest of the list shifts around it. */
.ep-step.is-dragging {
    opacity: .4;
    background: var(--fs-blue-soft);
}

.ep-step__number {
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--fs-muted);
    font-size: .9rem;
}

.ep-step__title { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }

/* The rule, on the row. Reading a path should not mean opening every step. */
.ep-step__waits {
    margin-top: .3rem;
    font-size: .85rem;
    color: var(--fs-violet);
}

/* A named stretch of the path, indented so it reads as a container rather than
   as another step. */
.ep-section {
    border-bottom: 1px solid var(--fs-line-soft);
    border-left: 3px solid var(--fs-line);
}

.ep-section__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
    padding: .6rem 1rem;
    background: var(--fs-canvas);
}

.ep-section__head i { color: var(--fs-muted); }

.ep-section__items { }
.ep-section__items .ep-step:last-child { border-bottom: none; }

/* The editor, opened per step. */
/*
    Open, the editor takes the whole row.

    Left in its own column it grew past the column edge and printed over the step
    beneath it, because a form is far wider than the button that opens it.
*/
.ep-step__edit { flex: none; }
.ep-step__edit[open] { flex: 1 0 100%; order: 9; }
.ep-step__edit[open] summary { margin-bottom: .5rem; }

.ep-step__edit summary { list-style: none; cursor: pointer; }
.ep-step__edit summary::-webkit-details-marker { display: none; }

.ep-edit {
    margin-top: .6rem;
    padding: .85rem;
    border: 1px solid var(--fs-line);
    border-radius: var(--fs-radius-sm);
    background: var(--fs-surface);
}

.ep-waits {
    margin-top: 1rem;
    padding: .75rem;
    border: 1px solid var(--fs-line-soft);
    border-radius: var(--fs-radius-sm);
    background: var(--fs-canvas);
}

.ep-waits legend {
    float: none;
    width: auto;
    font-size: .85rem;
    font-weight: 600;
    margin-bottom: .25rem;
    padding: 0;
}

/* Capped and scrolled: a long path would otherwise push the Save button off the
   screen behind a list of forty checkboxes. */
.ep-waits__list {
    max-height: 12rem;
    overflow-y: auto;
    display: grid;
    gap: .1rem;
}

.ep-waits__item {
    display: flex;
    align-items: baseline;
    gap: .5rem;
    padding: .25rem .4rem;
    border-radius: var(--fs-radius-sm);
    cursor: pointer;
}

.ep-waits__item:hover { background: var(--fs-blue-soft); }
.ep-waits__item input { margin-top: .15rem; }



/* Copy-to-another-year, tucked into the page header. */
.ep-copy { display: flex; gap: .35rem; align-items: center; }
.ep-copy select { width: auto; min-width: 7rem; }

/* The gathered rules card is gone; each row says what it waits for. */

/* The path's description, with Add a step at the end of the same row. */
.ep-intro {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}

/* Takes the space, so the button stays hard right however short the description is —
   and a path with no description still puts the button where the eye expects it. */
.ep-intro > span { flex: 1 1 20rem; min-width: 0; }

/* Renaming a form from the library: the field appears under the row's icon rail. */
.fb-rename { position: relative; display: inline-block; }
.fb-rename > summary { list-style: none; cursor: pointer; }
.fb-rename > summary::-webkit-details-marker { display: none; }

.fb-rename__form {
    position: absolute;
    right: 0;
    z-index: 5;
    display: flex;
    gap: .35rem;
    margin-top: .3rem;
    padding: .5rem;
    width: 22rem;
    max-width: 70vw;
    border: 1px solid var(--fs-line);
    border-radius: var(--fs-radius-sm);
    background: var(--fs-surface);
    box-shadow: 0 8px 20px rgba(16, 24, 40, .12);
}

.fb-rename__form input { flex: 1; min-width: 0; }

/* --------------------------------------------------- Starting a year checklist */

.soy { margin: 0; padding: 0; list-style: none; display: grid; gap: .5rem; }

.soy__step {
    display: flex;
    align-items: flex-start;
    gap: .85rem;
    padding: .9rem 1rem;
    border: 1px solid var(--fs-line-soft);
    border-radius: var(--fs-radius-sm);
    background: var(--fs-surface);
}

/* Done steps recede. The one still to do should be the thing the eye lands on. */
.soy__step.is-done { background: var(--fs-canvas); }

.soy__mark { flex: none; width: 1.5rem; text-align: center; color: var(--fs-line); }
.soy__step.is-done .soy__mark { color: var(--fs-green); }

.soy__body { flex: 1 1 20rem; min-width: 0; }

.soy__title {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
    margin-bottom: .2rem;
}

.soy__go { flex: none; white-space: nowrap; }

/* The application's one confirmation dialog. */
.fs-confirm {
    border: none;
    border-radius: var(--fs-radius);
    padding: 1.25rem;
    width: min(28rem, 92vw);
    color: var(--fs-ink);
    background: var(--fs-surface);
    box-shadow: 0 20px 45px rgba(16, 24, 40, .22);
}

.fs-confirm::backdrop { background: rgba(16, 24, 40, .45); }

/* ------------------------------------------------ Evaluator assignments */

.ea-filters { display: flex; flex-wrap: wrap; align-items: flex-end; gap: .5rem; }

/* The clear link sits on the same baseline as the fields, not their labels. */
.ea-filters__clear { margin-bottom: .15rem; }
.ea-filters__search { flex: 0 1 20rem; min-width: 10rem; }

/* The two pickers share what is left, so a chosen name has room to be read. */
.ea-filters .fs-tick { flex: 1 1 12rem; min-width: 9rem; }

/* The bar only exists while something is ticked, so it sits between the filters
   and the table rather than reserving space above them. */
.ea-bulk {
    padding: .85rem 1rem;
    background: var(--fs-blue-soft);
    border-top: 1px solid var(--fs-line-soft);
    border-bottom: 1px solid var(--fs-line-soft);
}

.ea-bulk__form { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.ea-bulk__form select, .ea-bulk__form input[type="text"] { width: auto; min-width: 10rem; }

/* A second evaluator sits under the first rather than beside it: they are two
   separate appointments, not one line of text. */
.ea-evaluator { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem; }
.ea-evaluator + .ea-evaluator { margin-top: .25rem; }

/* A search box pinned inside a checkbox menu, so scrolling the options does not
   scroll it away. */
.fs-tick__search {
    position: sticky;
    top: 0;
    z-index: 1;
    padding: .25rem .35rem .45rem;
    background: var(--fs-surface);
    border-bottom: 1px solid var(--fs-line-soft);
    margin-bottom: .25rem;
}

.fs-tick__none { padding: .35rem .5rem; }
.ea-bulk__pick { min-width: 14rem; }

/*
    The rows area keeps a floor, so narrowing the filters does not move the page.

    Without it, filtering three hundred rows down to none collapsed the card to a single
    line and everything below it jumped — with an open dropdown left hanging over
    nothing. A minimum height means the list empties in place.

    The real figure is measured and set in script, from where this area starts to the
    bottom of the window, so the card ends where the page ends whatever is above it. This
    is only the fallback for the moment before that runs.
*/
.ea-rows { min-height: 18rem; }

.ea-rows__empty {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 3rem 1.1rem;
}

/* Where a year begins, above the alphabetical list rather than inside it. */
.fs-settings__start {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 1rem 1.25rem;
    margin-bottom: 1.25rem;
    border: 1px solid var(--fs-blue);
    border-radius: var(--fs-radius);
    background: var(--fs-blue-soft);
    color: inherit;
    text-decoration: none;
}

.fs-settings__start:hover { color: inherit; box-shadow: var(--fs-shadow); }

.fs-settings__start-text { flex: 1 1 22rem; min-width: 0; }
.fs-settings__start-text strong { display: block; font-size: 1.05rem; }

/* ------------------------------------------------ Notification settings */

/*
    A list, not a stack of forms.

    Every rule used to be an open form on this page: twenty of them, each with a
    seven-way audience picker, and no way to see at a glance what the district
    actually sends. The settings moved into a dialog and the page became a list of
    sentences — the one thing somebody opening this screen wants to read.
*/
.fs-notif { list-style: none; margin: 0; padding: 0; }

.fs-notif__row {
    display: flex;
    align-items: center;
    gap: 1rem;

    /*
        Indented to sit under the programme's own heading.

        The heading is pushed right by the chevron that opens the section, so a rule
        starting at the card's edge began further left than the thing it belongs to —
        the list read as a set of items beside "Teacher evaluation" rather than under
        it. Three rem is where the title starts.

        The border still runs the full width: it separates rows, and a separator that
        starts partway across implies a second column that is not there.
    */
    padding: .75rem 1.1rem .75rem 3rem;
    border-top: 1px solid var(--fs-line-soft);
}

.fs-notif__row:first-child { border-top: none; }
.fs-notif__row:hover { background: var(--fs-neutral-soft); }

/* Off is dimmed, but only the summary — the name stays legible, because a reader
   scanning for a rule they turned off still has to be able to find it. */
.fs-notif__row.is-off .fs-notif__detail { opacity: .7; }

.fs-notif__main { min-width: 0; flex: 1; }

.fs-notif__name {
    display: flex;
    align-items: center;
    gap: .5rem;
    font-weight: 600;
    color: var(--fs-ink);
}

/* The summary wraps rather than truncating. It is a sentence about who gets told
   what, and half of that sentence is not worth having. */
.fs-notif__detail { margin-top: .15rem; }

.fs-notif__sent { flex: none; white-space: nowrap; }

.fs-notif__actions { display: flex; align-items: center; gap: .25rem; flex: none; }

/*
    An icon button that is still a 44px target.

    The icon is 16px and the padding does the rest, so the thing a finger has to
    find on a tablet is the size the rest of the application uses (spec section 96)
    even though what it looks like is a small glyph.
*/
.fs-iconbtn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    border: 1px solid transparent;
    border-radius: var(--fs-radius-control);
    background: none;
    color: var(--fs-muted);
    font-size: 1rem;
    transition: background-color .12s ease, color .12s ease, border-color .12s ease;
}

.fs-iconbtn:hover {
    background: var(--fs-surface);
    border-color: var(--fs-line);
    color: var(--fs-navy);
}

.fs-iconbtn--danger:hover { color: var(--fs-red); border-color: var(--fs-red); }

/* ------------------------------------------------------------- Dialogs */

/*
    The same shell as .fs-confirm, sized for a form rather than a question, and
    with the body scrolling instead of the page behind it.
*/
.fs-dialog {
    border: none;
    border-radius: var(--fs-radius);
    padding: 0;
    width: min(46rem, 94vw);
    max-height: 88vh;
    color: var(--fs-ink);
    background: var(--fs-surface);
    box-shadow: var(--fs-shadow-lg);
}

.fs-dialog::backdrop { background: rgba(0, 24, 66, .45); }

.fs-dialog form { display: flex; flex-direction: column; max-height: 88vh; }

.fs-dialog__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 1rem 1.25rem;
    border-bottom: 1px solid var(--fs-line-soft);
}

.fs-dialog__x {
    border: none;
    background: none;
    color: var(--fs-muted);
    font-size: 1rem;
    line-height: 1;
    min-width: 44px;
    min-height: 44px;
    border-radius: var(--fs-radius-control);
}

.fs-dialog__x:hover { background: var(--fs-neutral); color: var(--fs-navy); }

/* Only the middle scrolls, so the title and the Save button stay put on a short
   window — a form whose only Save has scrolled off the bottom reads as one that
   cannot be saved. */
.fs-dialog__body { padding: 1.25rem; overflow-y: auto; }

.fs-dialog__cols {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 1.5rem;
}

.fs-dialog__cols .form-check { margin-bottom: .35rem; }
.fs-dialog__cols .form-check-label { font-size: .84rem; }
.fs-dialog__cols .fs-hint { font-size: .74rem; }

.fs-dialog__gloss { margin: 0; font-size: .78rem; }
.fs-dialog__gloss dt { font-weight: 600; color: var(--fs-ink); }
.fs-dialog__gloss dd { margin: 0 0 .5rem; color: var(--fs-muted); }

.fs-dialog__foot {
    display: flex;
    justify-content: flex-end;
    gap: .5rem;
    padding: .9rem 1.25rem;
    border-top: 1px solid var(--fs-line-soft);
    background: var(--fs-neutral-soft);
    border-radius: 0 0 var(--fs-radius) var(--fs-radius);
}

@media (max-width: 640px) {
    .fs-notif__row { flex-wrap: wrap; }
    .fs-notif__sent { order: 3; }
    .fs-notif__actions { margin-left: auto; }
}

/*
    A collapsible group of notifications.

    <details> rather than a scripted accordion: it opens and closes without
    JavaScript, it is in the tab order for free, and browser find-in-page can
    reach a closed one — which matters on a screen holding fifty-odd rows.
*/
.fs-group { padding: 0; }

.fs-group__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: .9rem 1.1rem;
    cursor: pointer;
    list-style: none;
    border-radius: var(--fs-radius) var(--fs-radius) 0 0;
}

/* Safari draws its own triangle through ::-webkit-details-marker, which the
   standard list-style rule above does not reach. */
.fs-group__head::-webkit-details-marker { display: none; }

/* The chevron and the title travel together on the left; only the count goes
   right. Without this the three children share the row evenly and the heading
   floats into the middle of the card. */
.fs-group__head > div { flex: 1 1 auto; min-width: 0; }

.fs-group__head:hover { background: var(--fs-neutral-soft); }
.fs-group[open] .fs-group__head { border-bottom: 1px solid var(--fs-line-soft); }

.fs-group__title {
    margin: 0;
    font-size: 1.05rem;
    color: var(--fs-ink);
}

/* The affordance, drawn rather than borrowed: a chevron that turns when the
   group opens, so the head reads as something you can act on. */
.fs-group__head::after {
    content: "";
    order: -1;
    width: .5rem;
    height: .5rem;
    border-right: 2px solid var(--fs-muted);
    border-bottom: 2px solid var(--fs-muted);
    transform: rotate(-45deg);
    transition: transform .15s ease;
    flex: none;
    margin-right: .35rem;
}

.fs-group[open] .fs-group__head::after { transform: rotate(45deg); }

/* Holds the row's rhythm where a configured row has a bin and this one does not.
   Without it the edit icon slides right on every unconfigured row. */
.fs-iconbtn--void { display: inline-block; }


/*
    Movement is decoration; colour and shadow carry the meaning.

    So a reader who has asked their system for less of it loses the transforms
    and the transitions and keeps every state — the border still changes, the
    shadow still lifts, the focus ring still appears. Nothing is communicated
    only by something moving.
*/
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .001ms !important;
        scroll-behavior: auto !important;
    }

    /* Lifts and slides specifically, which are the ones that move the page
       under somebody rather than just changing how it looks. */
    .fs-card:hover, .qc-tile:hover, .fs-stat:hover, .fs-app-tile:hover {
        transform: none !important;
    }
}

/* ------------------------------------------------------- App switcher */

/*
    The 3x3 grid beside the staff name.

    <details> is the control, so it opens and closes and takes focus without any
    script — and the panel is positioned off the summary rather than off the bar,
    so it stays with its button at any width.
*/
/*
    Positioned against the user area, not against its own button.

    The button is not the last thing in the bar — the bell and Sign out sit to its
    right — so anchoring the panel to the button's right edge pushed a 320px panel
    ninety pixels off the left of a phone screen. The user area's right edge is
    near the edge of the header at every width, which is where a menu like this
    belongs anyway.
*/
.app-user { position: relative; }

/* The grid button. It used to be the <summary> of a <details> menu; it now opens
   the Frontier Apps panel, and keeps the same look beside the bell. */
.app-apps-button {
    display: grid;
    place-items: center;
    width: 38px;
    height: 38px;
    padding: 0;
    border: 0;
    border-radius: var(--fs-radius-control);
    background: transparent;
    color: #c6d5e8;
    cursor: pointer;
}

.app-apps-button:hover { background: rgba(255, 255, 255, .14); color: #fff; }
.app-apps-button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

/*
    The tile face. Shared with the admin screen, which is the point: an
    administrator setting the initials sees the tile staff will see.
*/
.fs-app-mark {
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    flex: none;
    border-radius: var(--fs-radius-control);
    background: var(--fs-navy);
    color: var(--fs-gold);
    font-weight: 800;
    font-size: 1rem;
    letter-spacing: .01em;
}

/* --------------------------------------------- App switcher admin rows */

/*
    Each application is its own form so a row saves alone, which means the rows
    cannot be table rows — a <form> inside a <table> is dropped by the parser.
    A grid does the aligning instead, and both buttons live inside that one form:
    Remove is a submit with a formaction, not a second form beside it.
*/
.fs-applinks { padding: 0 0 .5rem; }

.fs-applinks__head,
.fs-applinks__row {
    display: grid;
    grid-template-columns: 4.5rem 4.5rem minmax(0, 1fr) 12rem 8rem 6rem;
    gap: .6rem;
    padding: .7rem 1.1rem;
}

/* The column band, on the brand neutral like every other table header here. */
.fs-applinks__head {
    align-items: center;
    background: var(--fs-neutral);
    border-bottom: 1px solid var(--fs-line);
    font-size: .68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--fs-muted);
}

.fs-applinks__row {
    align-items: stretch;
    border-bottom: 1px solid var(--fs-line-soft);
}

/* Dimmed, but still legible: an application somebody turned off is one they
   still have to be able to find and read. */
.fs-applinks__row.is-off { background: var(--fs-neutral-soft); }
.fs-applinks__row.is-off .fs-applinks__main { opacity: .7; }

/*
    The single-line controls sit at the top of the row, level with the name they
    belong to. Only the actions column stretches — it is the one thing that wants
    the row's full height, so Save can sit at the top of it and Remove at the
    bottom. Left to stretch, the order and tile boxes grew into three-line-tall
    inputs holding one character.
*/
.fs-applinks__row > input,
.fs-applinks__row > select { align-self: start; }

.fs-applinks__tile { text-align: center; font-weight: 700; }

/* Name, address and description stack in one column: they are one thing being
   described, and three side-by-side boxes made the row unreadable. */
.fs-applinks__main { display: grid; gap: .35rem; min-width: 0; align-content: start; }

.fs-applinks__options {
    display: flex;
    flex-direction: column;
    gap: .4rem;
    align-items: flex-start;
}

/* Save at the top of the row, Remove at the bottom, both hard right — so the
   destructive one is never under the pointer on the way to the safe one. */
.fs-applinks__actions {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    align-items: stretch;
    gap: .4rem;
}

@media (max-width: 1100px) {
    .fs-applinks__head { display: none; }

    .fs-applinks__row {
        grid-template-columns: 4.5rem minmax(0, 1fr);
        row-gap: .5rem;
        padding: .9rem 1rem;
    }

    /* Once the columns collapse, everything after the tile takes the full width
       rather than squeezing into a 4.5rem gutter. */
    .fs-applinks__row > select,
    .fs-applinks__options,
    .fs-applinks__actions { grid-column: 2; }

    .fs-applinks__options { flex-direction: row; flex-wrap: wrap; gap: 1rem; }
    .fs-applinks__actions { flex-direction: row; justify-content: flex-end; }
}

/* ------------------------------------------------------- Account menu */

/*
    Who you are signed in as, and the way out.

    Sign out used to be a button on the bar itself: permanent space for the
    least-used control on the header, one stray click from the notification bell.
    It lives under the name now, which is where every application the district
    uses keeps it.
*/
.app-me { position: relative; }

.app-me > summary {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .25rem .5rem .25rem .25rem;
    border-radius: 999px;
    cursor: pointer;
    list-style: none;
    color: #e8eef7;
}

.app-me > summary::-webkit-details-marker { display: none; }

.app-me > summary:hover,
.app-me[open] > summary { background: rgba(255, 255, 255, .14); color: #fff; }

.app-me > summary:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

/* Gold on navy, the one pairing the brand allows for small text — and the same
   treatment as the application tiles, so the two read as one family. */
.app-me__avatar {
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    flex: none;
    border-radius: 50%;
    background: var(--fs-navy-soft);
    color: var(--fs-gold);
    font-size: .82rem;
    font-weight: 700;
}

.app-me__name {
    font-size: .85rem;
    font-weight: 600;
    white-space: nowrap;
}

/* Drawn rather than a glyph, so it turns with the menu. */
.app-me__caret {
    width: .4rem;
    height: .4rem;
    flex: none;
    margin-left: .1rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: translateY(-2px) rotate(45deg);
    transition: transform .15s ease;
    opacity: .8;
}

.app-me[open] .app-me__caret { transform: translateY(1px) rotate(-135deg); }

.app-me__panel {
    position: absolute;
    top: calc(100% + .5rem);
    right: 0;
    z-index: 1040;
    width: min(17rem, 92vw);
    padding: .85rem;
    background: var(--fs-surface);
    border: 1px solid var(--fs-line);
    border-radius: var(--fs-radius);
    box-shadow: var(--fs-shadow-lg);
}

/* An address wraps rather than widening the panel or being clipped: a long
   district address is common and half of one is no use. */
.app-me__email {
    margin: 0 0 .6rem;
    font-size: .8rem;
    color: var(--fs-muted);
    overflow-wrap: anywhere;
}

/*
    The groups somebody holds.

    Listed because in this application the answer to "why can I not see that tab?"
    is almost always the group, and this is the first place anybody looks.
*/
.app-me__roles {
    display: flex;
    flex-wrap: wrap;
    gap: .3rem;
    margin-bottom: .85rem;
    padding-bottom: .85rem;
    border-bottom: 1px solid var(--fs-line-soft);
}

/* Below 560px the name is dropped and the avatar carries the menu on its own —
   a full name plus a caret is most of a phone's header. */
@media (max-width: 560px) {
    .app-me__name { display: none; }
    .app-me > summary { padding-right: .35rem; }
}

/* --------------------------------------- Observed / not observed (walkthrough) */

/*
    The walkthrough answer to a framework component.

    A short visit can say honestly whether it saw something; it cannot support a
    judgement of how well it was done. So this replaces the rubric's four columns
    with two buttons, and the same field carries both instruments — see the note
    in _ComponentRating.
*/
.fp-observed { display: flex; flex-wrap: wrap; gap: .5rem; }

.fp-observed__choice { position: relative; }

/* Off-screen rather than display:none, so it keeps its place in the tab order and
   a screen reader still meets a radio group. */
.fp-observed__input {
    position: absolute;
    opacity: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    cursor: pointer;
}

.fp-observed__button {
    display: inline-flex;
    align-items: center;
    gap: .4rem;

    /* The 44px target the rest of the application uses: this is pressed on a
       tablet, standing up, in somebody's classroom. */
    min-height: 44px;
    padding: .5rem 1rem;

    border: 1px solid var(--fs-line-strong);
    border-radius: var(--fs-radius-control);
    background: var(--fs-surface);
    color: var(--fs-body);
    font-size: .9rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}

.fp-observed__button:hover { border-color: var(--fs-navy); background: var(--fs-neutral-soft); }

/*
    Chosen states, and they are deliberately different colours.

    Observed and not observed are both real answers, so neither is styled as an
    error — but they must not look alike either, or a row of them cannot be
    scanned. The icon carries the difference as well as the fill, because colour
    alone never carries a state here.
*/
.fp-observed__input:checked + .fp-observed__button {
    border-color: var(--fs-navy);
    background: var(--fs-blue-soft);
    color: var(--fs-navy);
    box-shadow: inset 0 0 0 1px var(--fs-navy);
}

.fp-observed__input:checked + .fp-observed__button--yes {
    border-color: var(--fs-green);
    background: var(--fs-green-soft);
    color: var(--fs-green);
    box-shadow: inset 0 0 0 1px var(--fs-green);
}

.fp-observed__input:focus-visible + .fp-observed__button {
    outline: 2px solid var(--fs-navy);
    outline-offset: 2px;
}

.fp-observed__input:disabled + .fp-observed__button {
    background: var(--fs-neutral);
    color: var(--fs-muted);
    cursor: default;
}

.fp-observed__guide { margin-top: .5rem; }

/* ------------------------------------------------------- Filter rows */

/*
    Seven filters across, on one row.

    Not a twelve-column grid: seven does not divide into twelve, which is why these
    were on two rows with a ragged break after the fifth. The tracks are sized to
    what each control actually holds — the search box and the job title take real
    typing, the rest are short lists — and minmax lets them shrink together rather
    than one of them collapsing first.
*/
.fs-filters {
    display: grid;
    grid-template-columns:
        minmax(0, 1.5fr)   /* Search         */
        minmax(0, 1fr)     /* Location       */
        minmax(0, 1.1fr)   /* Department     */
        minmax(0, 1fr)     /* Job type       */
        minmax(0, 1.1fr)   /* Security group */
        minmax(0, 1.3fr)   /* Job title      */
        minmax(0, 1fr);    /* Email domain   */
    gap: .5rem;
    align-items: end;
}

/* Two rows of the same widths before one long row of unreadably narrow controls,
   then a single column once even that stops working. */
@media (max-width: 1400px) {
    .fs-filters { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

@media (max-width: 900px) {
    .fs-filters { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 560px) {
    .fs-filters { grid-template-columns: minmax(0, 1fr); }
}

/*
    A region waiting on its replacement.

    Dimmed and inert rather than emptied or spinner-covered: the previous answer is
    still the best thing to show while the next one is fetched, and blanking it makes
    every keystroke flash the page. Pointer events go so nothing is clicked in the
    moment before it is replaced by a different row.
*/
.fs-region-busy {
    opacity: .55;
    pointer-events: none;
    transition: opacity .12s ease;
}

@media (prefers-reduced-motion: reduce) {
    .fs-region-busy { transition: none; }
}

/* --------------------------------------------- Settings, in two groups */

/*
    Setting a year up is one job done once in a fixed order; everything else is
    opened when something needs changing. They were one alphabetical grid of
    eighteen, which put Framework beside Forms beside Goals with nothing to say
    that three of them are steps one, two and seven of the same task.
*/
.fs-settings__group {
    margin: 2rem 0 .25rem;
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--fs-ink);
}

/* The first heading follows the checklist banner, which already has its own
   space below it. */
.fs-settings__group:first-of-type { margin-top: 1.5rem; }

.fs-settings__groupHint { margin-bottom: .9rem; }

/* ------------------------------------------- Moving a folder in a path */

/*
    The folder head is a grip.

    Steps could always be dragged; the folders holding them could not, so the order
    of the sections was whatever order they happened to be built in — and nothing
    could be placed above the first one.
*/
.ep-section__grip {
    border: none;
    background: none;
    padding: 0 .1rem;
    flex: none;
    line-height: 1;
    color: var(--fs-line-strong);
    cursor: grab;
    border-radius: var(--fs-radius-sm);
}

.ep-section__grip:active { cursor: grabbing; }
.ep-section__head:hover .ep-section__grip { color: var(--fs-muted); }

.ep-section__grip:focus-visible {
    outline: 2px solid var(--fs-navy);
    outline-offset: 2px;
    color: var(--fs-navy);
}

/* The same six dots the step rows use, so the two read as the same affordance at
   two scales rather than as two different controls. */
.ep-section__dots {
    display: block;
    width: 10px;
    height: 16px;
    background-image: radial-gradient(currentColor 1.1px, transparent 1.2px);
    background-size: 5px 5px;
    background-position: 0 2px;
}

/*
    Move a folder without dragging it.

    A drag can only reach what is on the screen, and a path runs longer than the
    window — so the folder at the bottom could not be dragged to the top however
    patiently. These sit at the end of the head, quiet until the row is hovered.
*/
.ep-section__moves {
    display: flex;
    gap: .15rem;
    margin-left: auto;
    flex: none;
}

.ep-section__move {
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--fs-radius-sm);
    background: none;
    color: var(--fs-muted);
    line-height: 1;
}

.ep-section__head:hover .ep-section__move { border-color: var(--fs-line); }
.ep-section__move:hover:not(:disabled) { background: var(--fs-neutral); color: var(--fs-ink); }

.ep-section__move:focus-visible {
    outline: 2px solid var(--fs-navy);
    outline-offset: 1px;
}

/* Kept legible rather than greyed to 1.2:1 — a disabled control still has to say
   which one it is. */
.ep-section__move:disabled {
    color: var(--fs-line-strong);
    border-color: transparent;
    cursor: default;
}

/*
    The unsaved-order bar.

    Sticky at the foot of the window because the arrangement it is describing may be
    two screens long, and an offer to save it that scrolls off the page is no offer.
*/
.ep-savebar {
    position: sticky;
    bottom: .75rem;
    z-index: 5;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .75rem;
    margin-top: .9rem;
    padding: .7rem .9rem;
    border: 1px solid var(--fs-gold-dark);
    border-radius: var(--fs-radius);
    background: var(--fs-gold-soft);
    box-shadow: var(--fs-shadow-lg);
}

.ep-savebar__text {
    flex: 1 1 14rem;
    min-width: 0;
    font-weight: 600;
    color: var(--fs-gold-ink);
}

.ep-savebar .btn { flex: none; }

/* A folder mid-move: lifted off the list, and the steps inside it dimmed so the
   block reads as one thing being carried rather than several. */
.ep-section.is-dragging {
    opacity: .6;
    cursor: grabbing;
    box-shadow: var(--fs-shadow-lg);
}

@media (prefers-reduced-motion: reduce) {
    .ep-section.is-dragging { box-shadow: none; }
}

/*
    A value changed from the row it is in.

    Shaped like the pill it replaces rather than like a form control: fifty of these
    down a column read as fifty input boxes and the table stops being a table. The
    caret is what says it can be changed; hover and focus fill the rest in.
*/
.fs-cellselect {
    max-width: 11rem;
    padding: .16rem 1.4rem .16rem .5rem;
    border: 1px solid transparent;
    border-radius: 999px;
    background-color: #e9edf5;
    color: var(--fs-blue-dark);
    font-size: .74rem;
    font-weight: 600;
    line-height: 1.5;
    appearance: none;

    /* The caret, drawn rather than fetched: one more request for a 9px triangle
       repeated down every row is not worth making. */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%2314203a' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right .45rem center;
    background-size: 9px 6px;
}

.fs-cellselect:hover:not(:disabled) { border-color: var(--fs-line-strong); }

.fs-cellselect:focus-visible {
    outline: 2px solid var(--fs-navy);
    outline-offset: 1px;
}

/* The two that are almost always already right. Same control, less colour, so the
   job type stays the thing the eye lands on in a row of three of these. */
.fs-cellselect--quiet {
    background-color: var(--fs-line-soft);
    color: var(--fs-muted);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%235c6880' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/* Missing, and the one value worth noticing on the way past. */
.fs-cellselect.fs-cellselect--unset {
    background-color: var(--fs-amber-soft);
    color: var(--fs-amber);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23a5610b' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/* Mid-post. Kept legible rather than faded to nothing — the row still has to be
   readable while the answer is on its way. */
.fs-cellselect:disabled { opacity: 1; color: var(--fs-muted); cursor: progress; }

.fs-cellselect.is-saved {
    background-color: var(--fs-green-soft);
    color: var(--fs-green);
    transition: background-color .25s ease;
}

.fs-cellselect.is-failed {
    background-color: var(--fs-red-soft);
    color: var(--fs-red);
    border-color: var(--fs-red);
}

@media (prefers-reduced-motion: reduce) {
    .fs-cellselect.is-saved { transition: none; }
}

/*
    A column heading that sorts.

    The arrow is only drawn once a column is in use: six grey arrows across a heading
    row read as decoration, and the reader stops seeing the one that matters.
*/
th.fs-sortable {
    cursor: pointer;
    user-select: none;
    position: relative;
    padding-right: 1.1rem;
}

th.fs-sortable:hover { color: var(--fs-navy); }

th.fs-sortable:focus-visible {
    outline: 2px solid var(--fs-navy);
    outline-offset: -2px;
}

th.fs-sortable::after {
    content: "";
    position: absolute;
    right: .35rem;
    top: 50%;
    width: 8px;
    height: 5px;
    margin-top: -2px;
    background: currentColor;
    opacity: 0;
    clip-path: polygon(50% 0, 100% 100%, 0 100%);
}

/* Faint on hover, so the column says it can be sorted before it is. */
th.fs-sortable:hover::after { opacity: .35; }

th.fs-sortable[aria-sort="ascending"]::after { opacity: .9; }

th.fs-sortable[aria-sort="descending"]::after {
    opacity: .9;
    clip-path: polygon(0 0, 100% 0, 50% 100%);
}

/*
    A table whose columns do not move.

    The default layout measures the widest cell in each column and sizes to that,
    which means the columns depend on which rows are showing — so sorting a list, or
    turning to the next page, silently re-laid out the whole table. Fixed layout
    takes the widths from the colgroup instead and ignores the content.

    Long values then have to be told what to do, because nothing is going to widen
    the column for them: wrap on a word boundary where they can, break mid-word only
    when a single word is longer than its column.
*/
/*
    Still a table at every width.

    The narrow-screen rule further up turns .fs-table into a block so it can scroll,
    and a block is not a table box — the colgroup widths stop applying and the
    columns start moving again on small screens. This one scrolls inside its own
    wrapper instead, so it can stay a table.
*/
.fs-table--fixed {
    table-layout: fixed;

    /* Below this the percentages stop being enough for the narrowest columns — the
       counts lose a digit and the button in the last one is clipped. It scrolls in
       its own wrapper instead. */
    min-width: 80rem;
    display: table;
    white-space: normal;
}

.fs-table--fixed th,
.fs-table--fixed td {
    overflow-wrap: break-word;
    word-break: normal;
}

/* The heading's own sort arrow must not be what pushes a column over. */
.fs-table--fixed th .fs-sort {
    display: inline-flex;
    align-items: baseline;
    gap: .25rem;
}

/* The row's own controls stay on one line; the column is sized for them. */
.fs-table--fixed td:last-child { white-space: nowrap; }

/*
    Every other row tinted.

    A list of a hundred and forty across nine columns is read across, and the eye
    loses the line somewhere around the fourth column. The tint is barely there —
    enough to follow, not enough to read as a state — and hover still wins over it,
    so the row under the pointer is always the strongest thing on the page.
*/
.fs-table--zebra tbody tr:nth-child(even) { background: var(--fs-neutral-soft); }
.fs-table--zebra tbody tr:hover { background: var(--fs-line-soft); }

/* Nothing special for a deactivated row: it is dimmed by opacity, not by a
   background, so it takes its stripe like every other row and the pattern stays
   regular. */

/*
    The save status, under the buttons it belongs to.

    It used to sit in the visit bar between Pause and End walkthrough — a line of text
    that changes on its own, in the middle of the run of controls the hand travels
    across. Here it is a caption for the row above it: glanced at, not read past.
*/
.ow-save--header {
    /* The second row of the aside column, so it lands under the buttons rather than
       beside the last of them. Relying on a flex row to wrap put it back on the same
       line, because the row is only as wide as the buttons make it. */
    margin-top: .35rem;
    text-align: right;
    font-size: .78rem;
    color: var(--fs-muted);
}

/*
    The buttons and the line under them, held to the right on both halves.

    Without this the group started wherever the title ended, and the two halves of a
    visit have titles of different lengths — so switching tabs slid the buttons across
    the page.
*/
.fs-visit-head__aside {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    margin-left: auto;
    flex: none;
}

.ow-save--header .bi { margin-right: .2rem; }

/* Failure is the one state worth interrupting for, wherever this sits. */
.ow-save--header.ow-save--error { color: var(--fs-red); font-weight: 600; }

/*
    Full screen: everything except the writing surface and the bar.

    The header — back link, title, the three record buttons, the tabs and the row of
    facts — is what an evaluator reads once at the start of a visit and then never
    again. On a laptop in somebody else's classroom it is a third of the window.
*/
body.ow-zen .fs-visit-head--full,
body.ow-zen .fs-visit-head--form { display: none; }

/*
    The save line, readable on white.

    Its colours were chosen for the navy bar it used to sit in — pale mint, pale
    amber — and on the canvas behind the header they are barely there. Same three
    states, in the ink the rest of the application uses for them.
*/
/*
    Read in ordinary ink.

    The three colours were for the navy bar this used to sit in. On the canvas they
    were pale enough to be missed, and a green "saved" beside an amber "not saved
    yet" on the other tab read as an alarm about work at risk when nothing was wrong.
    It is a caption, so it is coloured like one.

    Failure keeps its red. That one is not a caption — it is the only state where
    something has to be done about it.
*/
.ow-save--header { color: var(--fs-ink); }
.ow-save--header.ow-save--saved,
.ow-save--header.ow-save--saving { color: var(--fs-ink); }
.ow-save--header.ow-save--error { color: var(--fs-red); font-weight: 600; }

/* --- The tokens a notification's wording may use --------------------- */

.fs-tokens {
    list-style: none;
    margin: .5rem 0 0;
    padding: 0;

    /* Two columns where there is room: thirteen tokens in one column pushes the
       Save button below the fold of a dialog. */
    columns: 2;
    column-gap: 1.5rem;
}

.fs-tokens li {
    break-inside: avoid;
    margin-bottom: .3rem;
    font-size: .85rem;
}

.fs-tokens code {
    background: #f1eeed;
    border-radius: 4px;
    padding: .05rem .3rem;
    color: var(--fs-navy, #0a295c);
}

@media (max-width: 575.98px) {
    .fs-tokens { columns: 1; }
}

/* --- The wording in use, shown beside the boxes that replace it ------- */

.fs-sample {
    border: 1px solid var(--fs-line);
    border-radius: var(--fs-radius-sm);
    background: #fbf9fa;
    padding: .7rem .8rem;
}

.fs-sample__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    margin-bottom: .5rem;
    font-size: .88rem;
}

.fs-sample__body { margin: 0; }

.fs-sample__body dt {
    font-size: .75rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--fs-muted);
    font-weight: 600;
}

.fs-sample__body dd { margin: .15rem 0 .6rem; }

/* The template is shown as code because that is what it is — the braces are
   literal, and a proportional font makes them easy to miss. */
.fs-sample__body code {
    display: block;
    background: #fff;
    border: 1px solid var(--fs-line);
    border-radius: 4px;
    padding: .35rem .5rem;
    font-size: .82rem;
    color: var(--fs-ink);
    white-space: pre-wrap;
    word-break: break-word;
}

/* And what it turns into, so the pattern above can be read as a sentence. */
.fs-sample__reads {
    border-top: 1px dashed var(--fs-line);
    margin-top: .3rem;
    padding-top: .5rem;
    font-size: .85rem;
    display: grid;
    gap: .15rem;
}

/* ---------------------------------------------------- Frontier Apps panel
   A panel that floats in from the right rather than a dropdown, so the list
   can grow without running off the bottom of the screen. Inset from the edges
   with rounded corners, which is what makes it read as a surface of its own.

   Bootstrap's offcanvas does opening, the focus trap, Escape and the
   backdrop; everything here is appearance. */
.fa-panel {
    --fa-navy:         var(--fs-navy, #001842);
    --fa-navy-soft:    var(--fs-navy-soft, #0b2a5c);
    --fa-gold:         var(--fs-gold, #ffb504);
    --fa-surface:      var(--fs-surface, #ffffff);
    --fa-canvas:       var(--fs-canvas, #fffcff);
    --fa-ink:          var(--fs-body, #14203a);
    --fa-muted:        var(--fs-muted, #5c6880);
    --fa-line:         var(--fs-line-soft, #e6e3e6);
    --fa-neutral:      var(--fs-neutral, #ece7e5);
    --fa-neutral-dark: var(--fs-neutral-dark, #ddd6d3);
    --fa-radius:       var(--fs-radius-lg, 20px);
    --fa-radius-tile:  var(--fs-radius-control, 11px);
    --fa-inset: .75rem;

    --bs-offcanvas-width: min(380px, calc(100vw - 1.5rem));
    --bs-offcanvas-bg: var(--fa-surface);
    --bs-offcanvas-border-width: 0;

    height: auto;
    border-radius: var(--fa-radius);
    overflow: hidden;
    color: var(--fa-ink);
    box-shadow:
        0 24px 60px rgba(0, 24, 66, .28),
        0 4px 14px rgba(0, 24, 66, .12);
    /* An ease-out that settles rather than stops. */
    transition: transform .32s cubic-bezier(.22, .9, .24, 1);
}

/* Two classes, not one. Bootstrap's .offcanvas-end sets top: 0 and right: 0 and
   transform: translateX(100%) at single-class weight; written on .fa-panel alone
   the inset loses and the rounded corners press flat against the window.

   Closed, it must clear its inset as well as its width, or the rounded edge
   sits a few pixels inside the window. Opening and open are restated at the
   same weight so this rule does not hold the panel shut. */
.fa-panel.offcanvas-end {
    top: var(--fa-inset);
    right: var(--fa-inset);
    bottom: var(--fa-inset);
    transform: translateX(calc(100% + 2 * var(--fa-inset)));
}
.fa-panel.offcanvas-end.showing,
.fa-panel.offcanvas-end.show:not(.hiding) { transform: none; }

/* The page behind softens rather than going dark: this is a quick hop to
   another tool, not a dialog. Note this applies to every offcanvas backdrop
   in the application - see Gotchas. */
.offcanvas-backdrop.show { opacity: 1; background: rgba(0, 24, 66, .28); }
@supports (backdrop-filter: blur(2px)) {
    .offcanvas-backdrop.show { backdrop-filter: blur(2px); }
}

/* Navy header with a gold hairline, matching the top bar. */
.fa-panel .offcanvas-header {
    position: relative;
    padding: 1.05rem 1.15rem;
    background: var(--fa-navy);
    color: #fff;
    border-bottom: 0;
}
.fa-panel .offcanvas-header::after {
    content: "";
    position: absolute;
    left: 1.15rem;
    right: 1.15rem;
    bottom: 0;
    height: 2px;
    border-radius: 2px;
    background: var(--fa-gold);
    opacity: .9;
}
.fa-panel .offcanvas-title {
    margin: 0;
    font-size: 1rem;
    font-weight: 700;
    letter-spacing: .02em;
    color: #fff;
}

/* Bootstrap's close icon is drawn dark; on navy it has to be light. */
.fa-panel .btn-close {
    filter: invert(1) grayscale(1) brightness(2);
    opacity: .75;
    border-radius: 50%;
}
.fa-panel .btn-close:hover { opacity: 1; }
.fa-panel .btn-close:focus-visible { box-shadow: 0 0 0 3px rgba(255, 181, 4, .6); opacity: 1; }

.fa-panel .offcanvas-body {
    padding: 1.1rem 1rem 1.25rem;
    background: var(--fa-canvas);
    scrollbar-width: thin;
}

.fa-empty { margin: .25rem .15rem; font-size: .85rem; color: var(--fa-muted); }

.fa-section + .fa-section { margin-top: 1.25rem; }

/* Small uppercase label with a rule to the edge. */
.fa-group {
    display: flex;
    align-items: center;
    gap: .6rem;
    margin: 0 .15rem .6rem;
    font-size: .66rem;
    font-weight: 700;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--fa-muted);
}
.fa-group::after {
    content: "";
    flex: 1 1 auto;
    height: 1px;
    background: var(--fa-line);
}

.fa-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: .55rem;
}

/* Each application is a small card: white on the tinted body, lifting under
   the pointer. */
.fa-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .45rem;
    padding: .8rem .35rem .7rem;
    border-radius: var(--fa-radius-tile);
    background: var(--fa-surface);
    border: 1px solid var(--fa-line);
    text-decoration: none;
    color: var(--fa-navy);
    text-align: center;
    transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.fa-tile:hover {
    color: var(--fa-navy);
    border-color: transparent;
    transform: translateY(-2px);
    box-shadow: 0 8px 18px rgba(0, 24, 66, .12);
}
.fa-tile:focus-visible {
    outline: 2px solid var(--fa-gold);
    outline-offset: 2px;
    color: var(--fa-navy);
}

.fa-mark {
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    border-radius: 13px;
    background: linear-gradient(145deg, var(--fa-navy-soft), var(--fa-navy));
    color: var(--fa-gold);
    font-weight: 800;
    font-size: 1rem;
    letter-spacing: .01em;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .08);
}

.fa-name {
    font-size: .72rem;
    font-weight: 600;
    line-height: 1.25;
    overflow-wrap: anywhere;
}

.fa-footer {
    padding: .7rem 1rem .8rem;
    background: var(--fa-surface);
    border-top: 1px solid var(--fa-line);
    text-align: center;
}
.fa-footer a {
    display: inline-block;
    padding: .4rem .95rem;
    border-radius: 999px;
    font-size: .8rem;
    font-weight: 600;
    color: var(--fa-navy);
    text-decoration: none;
    background: var(--fa-neutral);
    transition: background .15s ease;
}
.fa-footer a:hover { background: var(--fa-neutral-dark); }

@media (max-width: 575.98px) {
    .fa-panel { --fa-inset: .5rem; }
}

@media (prefers-reduced-motion: reduce) {
    .fa-panel,
    .fa-tile { transition: none; }
    .fa-tile:hover { transform: none; }
}

/* --- A visit's header, sized for a laptop -------------------------------------

    At 1536 by 740 — a 1920 by 1080 laptop at the 125% Windows sets by default — the
    back link, title, buttons, tabs and facts took 273px, the evidence stream was left
    20px, and the Add evidence button ended below the bottom of a page that cannot
    scroll. The header is read once at the start of a visit; the stream and the box are
    used for the whole of it, so the header gives the room back.

    Both halves of the visit are tightened by the same amount, so switching between
    Quick Evidence and Full Form still moves nothing.
*/
.fs-visit-head--full { padding: .6rem 0 .5rem; }

/* The form half takes its space above from the main column's padding, the live half
   from its own. The difference is given back here so the two titles stay level. */
.fs-visit-head--form {
    margin-top: calc(.6rem - clamp(1rem, 1.6vw, 2rem));
    padding-bottom: .5rem;
}

.fs-visit-head .fs-back { margin-bottom: .1rem; }

@media (min-width: 576px) {
    .fs-visit-head h1 {
        font-size: 1.4rem;
        line-height: 1.25;
        margin-bottom: .1rem !important;
    }
}

/* The tabs and the facts share a row, wrapping onto two only when it runs out. */
.fs-visit-head__row { margin-top: .6rem; }

.fs-visit-head__row .fs-visitfacts { margin: 0 0 0 auto; }

.fs-visit-head__row .fs-visitfacts__item { flex: 0 0 clamp(10.5rem, 12vw, 15rem); }

/* The date needs room for its picker and its time without clipping. */
.fs-visit-head__row .fs-visitfacts__item:has(input[type="datetime-local"]) {
    flex-basis: clamp(13rem, 14vw, 17rem);
}

@media (max-width: 991.98px) {
    .fs-visit-head__row .fs-visitfacts { margin-left: 0; width: 100%; }
}

/*
    Thumb-sized targets for thumbs.

    Everything in a visit carries .fs-touch, a 44px minimum, because evaluators take
    tablets into classrooms — and that stays true there. With a mouse or a trackpad the
    same 44px is only height. A fine pointer gets 36px, still larger than Bootstrap's
    own buttons.
*/
@media (pointer: fine) {
    .ow-bar .fs-touch,
    .fs-visit-head .fs-touch,
    .ow-evidence .fs-touch {
        min-height: 36px;
        min-width: 36px;
    }

    /* The clock sets its own 44px rather than borrowing .fs-touch, and it is the
       tallest thing in the bar — so without it the bar stayed 60px however small
       the buttons beside it became. */
    .ow-bar .ow-timer { min-height: 36px; }

    .fs-visit-actions .btn,
    .fs-tabs__beside { min-height: 2.25rem; }

    .fs-tabs__tab { padding-block: .3rem; }

    .fs-visitfacts .form-control,
    .fs-visitfacts .form-select { height: 2.25rem; }
}

/* --- A visit on a phone: nothing wider than the screen --------------------------

    At 375px the page scrolled sideways by more than a hundred pixels, and a page that
    moves under the thumb in both directions is one where a tap meant for a note lands
    on the edge of something else. Each row that did it had a minimum width chosen for
    a laptop and nothing telling it a phone is narrower.
*/
@media (max-width: 575.98px) {
    /* Forms, History and Print were 120px each by rule: 376px in a 351px column. They
       share the row equally instead, and the save line still sits under them. */
    .fs-visit-head__aside {
        flex: 1 1 100%;
        width: 100%;
    }

    .fs-visit-actions { width: 100%; flex-wrap: nowrap; }

    .fs-visit-actions .btn {
        flex: 1 1 0;
        min-width: 0;
    }

    /* The two halves' tabs were 11rem each, wider together than the screen. Half
       each, and Sync takes the line under them. */
    .fs-visit-head__row .fs-tabs { width: 100%; }

    .fs-visit-head__row .fs-tabs__tab { min-width: 0; }
}

/* --- The header on a narrow phone: the name gives way, not the controls ---------

    At 360px the header row had 330px for a menu button, the product name and the
    bell, avatar and apps grid. The name was allowed to shrink and so was the group of
    controls — and the controls went first. The group squeezed from 142px to 102px,
    the grid button from 38px to 16px, and the button ended six pixels past the edge of
    the screen, so every page scrolled sideways on a common Android phone.

    The name already truncates with an ellipsis, which is what it is for. The controls
    keep their size and the name takes all of the shortfall.
*/
.app-topbar__product { min-width: 0; }

.app-user,
.app-apps-button { flex: none; }

/* --- A folder row stays one row -----------------------------------------------

    The five columns on the right are fixed widths, so the name column took whatever
    was left: on a 2000px screen it was 1032px, and a row read as a name on the far
    left and its status 812 pixels away on the far right, with nothing in between to
    carry the eye across. The table is capped instead, so a row stays something you
    can read in one go however wide the window gets.
*/
.fs-folder__inner { max-width: 70rem; }

/* --- Strategic plan goals inside a standard, on the summative page ------------

    Quiet enough to read as supporting evidence for the rating beside them rather
    than as a second instrument competing with it: no card of its own, one tinted
    block, the target in the muted ink the rest of the page uses for facts already
    known.
*/
.lp-goals {
    margin: 0 0 1rem;
    padding: .7rem .9rem .5rem;
    border-radius: var(--fs-radius-sm);
    background: var(--fs-neutral-soft, #f6f4f3);
    border: 1px solid var(--fs-line-soft);
}

.lp-goals__head {
    font-size: .72rem;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--fs-muted);
    margin-bottom: .5rem;
}

.lp-goals__year { font-weight: 600; letter-spacing: .04em; }

.lp-goal {
    display: flex;
    align-items: flex-start;
    gap: .6rem;
    padding: .3rem 0;
    cursor: pointer;
}

.lp-goal__text { display: grid; gap: .1rem; min-width: 0; }

.lp-goal__title { font-size: .88rem; color: var(--fs-ink); }

.lp-goal__target { font-size: .8rem; color: var(--fs-muted); }

.lp-goals__note {
    margin: .4rem 0 0;
    font-size: .75rem;
    font-style: italic;
    color: var(--fs-muted);
}

/* --- Checklist lines inside a standard, on the summative page -----------------

    A result and a practice look different because they are answered differently:
    the first with a state nobody can know in October, the second with a tick. The
    target sits to the right of the line it belongs to, so a column of subjects can
    be read down in one pass — mathematics met, English not — which is the whole
    reason each subject is its own line.
*/
.lp-goals__ind {
    margin: .7rem 0 .2rem;
    font-size: .78rem;
    font-weight: 700;
    color: var(--fs-navy);
}

.lp-goals__sub {
    margin: .45rem 0 .1rem;
    font-size: .74rem;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--fs-muted);
}

.lp-item {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .22rem 0;
    border-bottom: 1px solid var(--fs-line-soft);
}

.lp-item:last-of-type { border-bottom: 0; }

.lp-item__text {
    display: flex;
    flex: 1 1 auto;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .15rem .6rem;
    min-width: 0;
}

.lp-item__title { font-size: .88rem; color: var(--fs-ink); }

/* The number, held to the right so a run of subjects lines up. */
.lp-item__target {
    margin-left: auto;
    font-size: .85rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--fs-navy);
    white-space: nowrap;
}

.lp-item__note { flex: 1 1 100%; font-size: .76rem; color: var(--fs-muted); }

.lp-item__state { flex: 0 0 9.5rem; }

@media (max-width: 575.98px) {
    .lp-item { flex-wrap: wrap; }
    .lp-item__state { flex: 1 1 100%; }
    .lp-item__target { margin-left: 0; }
}

/* --- Making the checklist scannable ------------------------------------------

    Forty rows of identical grey, each with the same dropdown, is a list nobody can
    track: finding the two things at risk means reading all forty. So each row takes
    the colour of its own answer, the rows alternate so the eye can hold a line
    across to its target, and each standard carries a bar and a count of what is
    still open.

    Colour is never the only signal — the state is also written in the control
    itself, which is what somebody who cannot tell the green from the amber reads.
*/
.lp-goals__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
}

.lp-tally {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    text-transform: none;
    letter-spacing: 0;
}

.lp-tally__bar {
    width: 7rem;
    height: 6px;
    border-radius: 999px;
    background: var(--fs-neutral-dark, #ddd6d3);
    overflow: hidden;
}

.lp-tally__fill {
    display: block;
    height: 100%;
    width: 0;
    border-radius: 999px;
    background: var(--fs-green, #2e7d52);
    transition: width .18s ease;
}

.lp-tally__text {
    font-size: .72rem;
    font-weight: 600;
    color: var(--fs-muted);
    white-space: nowrap;
}

[data-checklist][data-trouble="true"] .lp-tally__text { color: var(--fs-amber-ink, #8a5a00); }

/* --- The rows ---------------------------------------------------------- */

.lp-item {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .35rem .5rem;
    border-bottom: 1px solid var(--fs-line-soft);
    border-left: 3px solid transparent;
    border-radius: 2px;
}

.lp-item:nth-of-type(even) { background: rgba(0, 24, 66, .022); }

.lp-item:hover { background: rgba(0, 24, 66, .05); }

/* The answer, as a colour down the left edge. Quiet until it means something. */
.lp-item[data-state="met"],
.lp-item[data-state="done"] { border-left-color: var(--fs-green, #2e7d52); }

.lp-item[data-state="not_met"],
.lp-item[data-state="not_done"] { border-left-color: var(--fs-red, #b3261e); }

/* A practice that is done steps back: it needs no more attention this year. */
.lp-item[data-state="done"] .lp-item__title { color: var(--fs-muted); }

.lp-item__state {
    flex: 0 0 9.5rem;
    font-size: .82rem;
}

.lp-item[data-state="not_met"] .lp-item__state,
.lp-item[data-state="not_done"] .lp-item__state { border-color: var(--fs-red, #b3261e); }
.lp-item[data-state="met"] .lp-item__state { border-color: var(--fs-green, #2e7d52); }

/* --- Goal against actual, on one line ----------------------------------------

    Two figures in fixed columns so a run of subjects can be read straight down:
    goal, then what happened, then the word for it. Both are tabular numerals and
    both sit in the same width, because the eye compares them by position before it
    reads either of them.

    The labels are small and permanent rather than placeholder text — a placeholder
    disappears the moment somebody types, which is exactly when the column needs
    saying.
*/
.lp-item__goal,
.lp-item__actual {
    display: flex;
    align-items: baseline;
    gap: .35rem;
    flex: 0 0 auto;
}

.lp-item__label {
    font-size: .66rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--fs-muted);
}

.lp-item__figure {
    min-width: 5.5rem;
    font-size: .85rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--fs-navy);
}

.lp-item__actual .form-control {
    width: 6rem;
    font-size: .85rem;
    font-variant-numeric: tabular-nums;
}

/* The figure that decides it, marked once it is in. */
.lp-item__actual .form-control:not(:placeholder-shown),
.lp-item__actual .form-control:focus { font-weight: 600; }

@media (max-width: 767.98px) {
    .lp-item__goal,
    .lp-item__actual { flex: 1 1 auto; }

    .lp-item__figure { min-width: 0; }

    .lp-item__actual .form-control { width: 100%; }
}

/* --- The checklist row as a grid ---------------------------------------------

    Five fixed columns, so thirteen lines under one indicator can be read straight
    down: marker, item, goal, actual, answer. A grid rather than a flex row because
    the cells are not all filled — a practice has no figure — and with flex an empty
    cell collapses and drags the column out of line on exactly the rows where
    alignment is doing the work.

    The marker is there so the eye finds the start of a line, and so a line that
    wraps still reads as one item. It carries the state colour as well as the left
    border, which is two signals for the same thing and deliberate: the border is
    easy to miss at the width this card is usually read at.
*/
.lp-item {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) 7.5rem 7.5rem 9.5rem;
    align-items: center;
    gap: .75rem;
}

/*
    Each cell says which column it is in, rather than taking the next free one.
    A practice has no figures and so renders neither cell; left to flow, its tick
    would slide up into the goal column and the whole right edge would come apart
    on every other row.
*/
.lp-item__bullet { grid-column: 1; }
.lp-item__text { grid-column: 2; }
.lp-item__goal { grid-column: 3; }
.lp-item__actual { grid-column: 4; }
.lp-item__state { grid-column: 5; }

/* With no figures at all, the wording takes the room they would have had — these
   are the long lines, and three wrapped lines beside two empty columns is a waste
   of the width and harder to read. */
.lp-item[data-figures="no"] .lp-item__text { grid-column: 2 / 5; }

.lp-item__bullet {
    width: .45rem;
    height: .45rem;
    border-radius: 50%;
    background: var(--fs-navy);
    opacity: .3;
}

.lp-item[data-state="met"] .lp-item__bullet,
.lp-item[data-state="done"] .lp-item__bullet { background: var(--fs-green, #2e7d52); opacity: 1; }

.lp-item[data-state="not_met"] .lp-item__bullet,
.lp-item[data-state="not_done"] .lp-item__bullet { background: var(--fs-red, #b3261e); opacity: 1; }

/* The text is a block now, not a row of its own: the note belongs under the
   title rather than beside it. */
.lp-item__text { display: block; }

.lp-item__note { display: block; margin-top: .1rem; }

.lp-item__goal,
.lp-item__actual { flex: none; }

@media (max-width: 991.98px) {
    /* Narrow: back to a wrapping row, so the three controls move under the text
       together instead of each claiming a line of its own. */
    .lp-item {
        display: flex;
        flex-wrap: wrap;
        column-gap: .75rem;
        row-gap: .35rem;
    }

    .lp-item__bullet,
    .lp-item__text,
    .lp-item__goal,
    .lp-item__actual,
    .lp-item__state { grid-column: auto; }

    .lp-item__text { flex: 1 1 14rem; }

    .lp-item__state { margin-left: auto; }
}

/* --- The checklist tray, and columns that fit --------------------------------

    Two faults in the first cut.

    The tray sat on the same near-white as the card it is inside, so the one block
    on the page that has to be worked through read as part of the prose above it.
    It is a darker neutral now, with the rows banded in white on top: the list looks
    like a list from across the room.

    And the cells overflowed. "GOAL" plus a figure needed more than the column it
    was given, so it spilled to the right and the actual-value box was drawn under
    the progress control. The labels have moved to a single header row, the columns
    are sized to what is left, and every cell is allowed to shrink — a grid cell
    does not shrink below its content unless it is told it may.
*/
.lp-goals {
    background: var(--fs-neutral, #ece7e5);
    border-color: var(--fs-neutral-dark, #ddd6d3);
    padding: .7rem .75rem .55rem;
}

.lp-item {
    grid-template-columns: auto minmax(0, 1fr) 8rem 7rem 9.5rem;
    gap: .6rem;
    padding: .4rem .55rem;
}

.lp-item > * { min-width: 0; }

/* Rows banded in white on the darker tray, rather than shaded on a pale one. */
.lp-item:nth-of-type(even) { background: rgba(255, 255, 255, .5); }

.lp-item:hover { background: rgba(255, 255, 255, .85); }

.lp-item--head:nth-of-type(even),
.lp-item--head:hover { background: none; }

.lp-item--head {
    padding-top: .15rem;
    padding-bottom: .15rem;
    border-bottom: 1px solid var(--fs-neutral-dark, #ddd6d3);
    font-size: .66rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--fs-muted);
}

.lp-item__bullet--blank { background: none; }

.lp-item__answer-head { grid-column: 5; }

/* Said once in the header instead. */
.lp-item__label { display: none; }

.lp-item__figure { min-width: 0; }

.lp-item__actual .form-control { width: 100%; }

.lp-item__state { width: 100%; }

@media (max-width: 991.98px) {
    /* No columns to head, so the labels go back on the rows. */
    .lp-item--head { display: none; }

    .lp-item__label { display: inline; }

    .lp-item__actual .form-control { width: 6rem; }

    .lp-item__state { width: auto; }
}

/* --- Not applicable ----------------------------------------------------------

    A measure a campus does not have is answered, not unfinished — it counts
    towards the tally and takes no colour, because there is nothing here to be
    pleased or worried about. The row steps back the way a finished practice does.
*/
.lp-item[data-state="na"] { border-left-color: var(--fs-line); }

.lp-item[data-state="na"] .lp-item__bullet { background: var(--fs-muted); opacity: .45; }

.lp-item[data-state="na"] .lp-item__title,
.lp-item[data-state="na"] .lp-item__figure { color: var(--fs-muted); }

/* --- Stepping the list in under its headings ---------------------------------

    Three levels, each stepped in from the one above: the indicator, the group
    inside it, and the lines themselves. Everything sat on the same left edge
    before, so "Mathematics" and "Achievement — all students (MPI)" read as
    siblings and you had to use the type size to work out which was the heading.

    The column header steps in by the same amount as the rows, or its labels would
    stop sitting above the columns they name.
*/
.lp-goals__sub { padding-left: .85rem; }

.lp-item { padding-left: 1.6rem; }

@media (max-width: 991.98px) {
    /* Less room to give away, and the rows are taller there anyway. */
    .lp-goals__sub { padding-left: .5rem; }

    .lp-item { padding-left: .9rem; }
}

/* --- Starting a protocol over ------------------------------------------------

    The one control in the rail that destroys something, so it does not look like
    the others: closed by default, quiet until opened, and red only at the step
    that actually deletes. An always-visible red button next to "Print" would be
    both alarming and easy to hit by accident.
*/
.lp-reset {
    margin-top: .65rem;
    border-top: 1px solid var(--fs-line-soft);
    padding-top: .65rem;
}

.lp-reset > summary {
    font-size: .78rem;
    color: var(--fs-muted);
    cursor: pointer;
    list-style: none;
}

.lp-reset > summary::-webkit-details-marker { display: none; }

.lp-reset > summary::before {
    content: "⚠";
    margin-right: .35rem;
    opacity: .7;
}

.lp-reset > summary:hover { color: var(--fs-red, #b3261e); }

.lp-reset__body {
    display: flex;
    flex-direction: column;
    gap: .5rem;
    margin-top: .6rem;
}

.lp-reset__warn {
    margin: 0;
    font-size: .74rem;
    line-height: 1.45;
    color: var(--fs-muted);
}

.lp-reset__agree {
    display: flex;
    align-items: flex-start;
    gap: .45rem;
    font-size: .74rem;
    line-height: 1.35;
    color: var(--fs-ink);
    cursor: pointer;
}

.lp-reset__agree input { margin-top: .15rem; }

/* --- The principal checklist editor ------------------------------------------

    A list of forty measures that is read far more often than it is changed, so it
    is built as a document first and an editor second: the standards are the
    chapters, the indicators and their groups are headings inside them, and the
    controls stay quiet until you look for them.

    The earlier cut repeated the four column headings above every indicator, which
    is seven copies of the same furniture down one page and columns that never quite
    line up. The headings are said once per standard now.
*/

/* --- The page head and its toolbar -------------------------------------- */

.pg-head__lede { max-width: 54rem; margin-bottom: 0; }

.pg-bar {
    position: relative;
    display: flex;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
    margin: 1.1rem 0 1.4rem;
    padding: .7rem .9rem;
    border: 1px solid var(--fs-line-soft);
    border-radius: var(--fs-radius-sm);
    background: var(--fs-neutral-soft, #f5f2f1);
}

.pg-stats {
    display: flex;
    align-items: baseline;
    gap: 1.4rem;
    margin-right: auto;
}

.pg-stat { display: flex; align-items: baseline; gap: .35rem; }

.pg-stat__n {
    font-size: 1.05rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--fs-navy);
}

.pg-stat__l {
    font-size: .7rem;
    font-weight: 600;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--fs-muted);
}

/* Anything switched off is worth noticing on the way past. */
.pg-stat--warn .pg-stat__n { color: var(--fs-amber-ink, #8a5a00); }

.pg-bar__hint { margin: 0; font-size: .76rem; }

/*
    Adding happens from the heading the line will live under, so there is one of
    these on every standard and every indicator rather than one button at the top
    that then asks you where you meant.
*/
.pg-add-btn {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: .25rem .7rem;
    border-radius: var(--fs-radius-sm);
    background: var(--fs-navy);
    color: #fff;
    font-size: .76rem;
    font-weight: 600;
    text-decoration: none;
    white-space: nowrap;
}

.pg-add-btn:hover { background: var(--fs-navy-700, #0b1f4d); color: #fff; }

/*
    Always on show, never on hover.

    A control that appears when the pointer happens to be over the right row is one
    you have to already know about to find, and this is the way a goal gets added to
    an indicator — the main thing this page is for. It is quiet rather than hidden:
    an outlined chip in the heading's own colour, which reads as part of the heading
    until you want it.
*/
.pg-add-link {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    margin-left: .7rem;
    padding: .1rem .5rem;
    border: 1px solid var(--fs-line);
    border-radius: 999px;
    font-size: .7rem;
    font-weight: 600;
    color: var(--fs-muted);
    text-decoration: none;
    white-space: nowrap;
    vertical-align: middle;
}

.pg-add-link:hover,
.pg-add-link:focus-visible {
    border-color: var(--fs-navy);
    background: var(--fs-navy);
    color: #fff;
}

.pg-add-link i { font-size: .68rem; }

/* An indicator with nothing under it says so, because an empty space says
   nothing and this page is read to find the gaps. */
.pg-none-row > td {
    padding: .4rem .9rem .5rem 1.6rem;
    border-top: 1px solid var(--fs-line-soft);
    font-size: .76rem;
    font-style: italic;
    color: var(--fs-muted);
}

.pg-badge--warn { background: var(--fs-amber, #c77700); }

/* --- A standard, as a card ---------------------------------------------- */

.pg-card {
    margin-bottom: 1.5rem;
    border: 1px solid var(--fs-line-soft);
    border-radius: var(--fs-radius-sm);
    background: var(--fs-surface, #fff);
    overflow: visible;
}

.pg-card__head {
    display: flex;
    align-items: center;
    gap: .7rem;
    padding: .7rem .9rem;
    border-bottom: 1px solid var(--fs-line-soft);
    background: var(--fs-neutral-soft, #f5f2f1);
    border-radius: var(--fs-radius-sm) var(--fs-radius-sm) 0 0;
}

.pg-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.6rem;
    height: 1.6rem;
    border-radius: 50%;
    background: var(--fs-navy);
    color: #fff;
    font-size: .78rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.pg-card__title {
    margin: 0;
    font-size: .95rem;
    font-weight: 700;
    color: var(--fs-navy);
}

.pg-card__count {
    margin-left: auto;
    font-size: .74rem;
    font-weight: 600;
    color: var(--fs-muted);
    white-space: nowrap;
}

/* --- The table ----------------------------------------------------------- */

.pg-table {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
}

.pg-table > thead th {
    padding: .5rem .9rem;
    border-bottom: 1px solid var(--fs-line-soft);
    font-size: .66rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--fs-muted);
    text-align: left;
}

.pg-col-goal { width: 11rem; }
.pg-col-kind { width: 6.5rem; }
.pg-col-campus { width: 10rem; }
.pg-col-act { width: 7.5rem; }

/* The indicator and the group as rows, so the columns below them stay one set
   of columns rather than seven sets that happen to look alike. */
.pg-ind > th {
    padding: .85rem .9rem .3rem;
    position: relative;
    font-size: .8rem;
    font-weight: 700;
    color: var(--fs-navy);
    text-align: left;
}

.pg-ind__code { font-variant-numeric: tabular-nums; }

.pg-ind__title { font-weight: 500; color: var(--fs-ink); }

.pg-ind__code + .pg-ind__title::before {
    content: "·";
    margin: 0 .3rem;
    color: var(--fs-muted);
}

.pg-grp > th {
    padding: .5rem .9rem .15rem 1.6rem;
    font-size: .68rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--fs-muted);
    text-align: left;
}

.pg-item > td {
    padding: .45rem .9rem;
    border-top: 1px solid var(--fs-line-soft);
    vertical-align: middle;
}

.pg-item > td:first-child { padding-left: 1.6rem; }

.pg-item:hover > td { background: rgba(0, 24, 66, .03); }

/* Paused: still listed, because it is coming back. Stated in a tag as well as
   in the colour, which is what somebody who cannot see the grey reads. */
.pg-item--paused .pg-wording,
.pg-item--paused .pg-target,
.pg-item--paused .pg-figure__val { color: var(--fs-muted); }

.pg-wording { font-size: .86rem; }

.pg-note {
    display: block;
    margin-top: .1rem;
    font-size: .74rem;
    line-height: 1.4;
    color: var(--fs-muted);
}

.pg-target {
    font-size: .85rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--fs-navy);
}

.pg-none { color: var(--fs-muted); font-size: .8rem; }

/* A figure that differs by building, one line per building. */
.pg-figures { display: flex; flex-direction: column; gap: .1rem; }

.pg-figure { display: flex; gap: .35rem; font-size: .76rem; }

.pg-figure__code {
    min-width: 3rem;
    font-weight: 700;
    color: var(--fs-muted);
}

.pg-figure__val { font-variant-numeric: tabular-nums; color: var(--fs-navy); }

/* --- Tags ---------------------------------------------------------------- */

.pg-tag {
    display: inline-block;
    padding: .1rem .45rem;
    border-radius: 999px;
    font-size: .68rem;
    font-weight: 700;
    letter-spacing: .03em;
    white-space: nowrap;
}

.pg-tag--result { background: rgba(37, 99, 235, .1); color: #1d4ed8; }

.pg-tag--practice { background: rgba(46, 125, 82, .12); color: #1f6340; }

.pg-tag--paused {
    margin-left: .4rem;
    background: var(--fs-neutral-dark, #ddd6d3);
    color: var(--fs-muted);
}

.pg-campuses { display: flex; flex-wrap: wrap; gap: .25rem; }

.pg-campus {
    padding: .05rem .4rem;
    border: 1px solid var(--fs-line);
    border-radius: 999px;
    font-size: .68rem;
    font-weight: 600;
    color: var(--fs-ink);
    white-space: nowrap;
}

/* --- Row controls -------------------------------------------------------- */

.pg-actions {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: .1rem;
}

.pg-move { display: inline; }

/* Quiet until wanted: forty rows of outlined buttons read as a wall of boxes. */
.pg-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--fs-radius-sm);
    background: none;
    color: var(--fs-muted);
    font-size: .82rem;
    cursor: pointer;
    list-style: none;
}

.pg-icon::-webkit-details-marker { display: none; }

.pg-icon:hover,
.pg-icon:focus-visible {
    border-color: var(--fs-line);
    background: #fff;
    color: var(--fs-navy);
}

.pg-icon--danger:hover,
.pg-icon--danger:focus-visible {
    border-color: var(--fs-red, #b3261e);
    color: var(--fs-red, #b3261e);
}

.pg-pop[open] > .pg-icon {
    border-color: var(--fs-line);
    background: #fff;
    color: var(--fs-navy);
}

/* --- The forms ----------------------------------------------------------- */

/* --- The dialog ---------------------------------------------------------

    Adding, editing and removing all happen here. The form is eight fields wide
    and a table row is not, so it stops trying to be one.

    It is a state of the address rather than of a script: the backdrop and the
    close control are links back to the plain page, which means the browser's own
    back button closes it, a reload keeps it open, and a save that fails comes
    back with the dialog still on the line it was about.
*/
.pg-modal {
    position: fixed;
    inset: 0;
    z-index: 1080;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2rem 1rem;
}

.pg-modal__scrim {
    position: absolute;
    inset: 0;
    background: rgba(0, 16, 44, .45);
    backdrop-filter: blur(1.5px);
}

.pg-dialog {
    position: relative;
    display: flex;
    flex-direction: column;
    width: min(46rem, 100%);
    max-height: 100%;
    border-radius: calc(var(--fs-radius-sm) * 1.5);
    background: var(--fs-surface, #fff);
    box-shadow: 0 24px 60px rgba(0, 16, 44, .3);
    overflow: hidden;
}

.pg-dialog__head {
    display: flex;
    align-items: flex-start;
    gap: 1rem;
    padding: 1rem 1.25rem;
    border-bottom: 1px solid var(--fs-line-soft);
    background: var(--fs-neutral-soft, #f5f2f1);
}

.pg-dialog__title {
    margin: 0;
    font-size: 1rem;
    font-weight: 700;
    color: var(--fs-navy);
}

.pg-dialog__sub {
    margin: .15rem 0 0;
    font-size: .76rem;
    color: var(--fs-muted);
}

.pg-dialog__close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.9rem;
    height: 1.9rem;
    margin-left: auto;
    border-radius: var(--fs-radius-sm);
    color: var(--fs-muted);
    text-decoration: none;
    flex: none;
}

.pg-dialog__close:hover,
.pg-dialog__close:focus-visible {
    background: var(--fs-neutral-dark, #ddd6d3);
    color: var(--fs-navy);
}

/* The body scrolls; the head and the buttons stay put, so the thing that commits
   the change is never below the fold on a short window. */
.pg-dialog__body {
    padding: 1.1rem 1.25rem;
    overflow-y: auto;
}

.pg-dialog__foot {
    display: flex;
    justify-content: flex-end;
    gap: .5rem;
    padding: .85rem 1.25rem;
    border-top: 1px solid var(--fs-line-soft);
    background: var(--fs-neutral-soft, #f5f2f1);
}

.pg-remove__what {
    margin: 0 0 .5rem;
    font-size: .95rem;
    font-weight: 600;
    color: var(--fs-ink);
}

.pg-fields {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: .75rem 1rem;
}

.pg-field {
    display: flex;
    flex-direction: column;
    gap: .25rem;
    margin: 0;
    min-width: 0;
}

.pg-field--wide { grid-column: 1 / -1; }

.pg-field--tick {
    grid-column: 1 / -1;
    flex-direction: row;
    align-items: center;
    gap: .45rem;
    font-size: .82rem;
    cursor: pointer;
}

.pg-field__label {
    font-size: .68rem;
    font-weight: 700;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--fs-muted);
}

.pg-field__hint {
    font-weight: 500;
    text-transform: none;
    letter-spacing: 0;
    opacity: .85;
}

.pg-campuses-box {
    border: 1px solid var(--fs-line-soft);
    border-radius: var(--fs-radius-sm);
    padding: .6rem .7rem;
}

.pg-campuses-box__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
    gap: .5rem .9rem;
    margin-top: .35rem;
}

.pg-campus-field { display: flex; align-items: center; gap: .5rem; }

.pg-campus-field__tick {
    display: flex;
    align-items: center;
    gap: .35rem;
    margin: 0;
    font-size: .8rem;
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
}

.pg-campus-field__target { width: 6.5rem; font-size: .8rem; }

.pg-form__foot {
    display: flex;
    justify-content: flex-end;
    gap: .5rem;
    margin-top: .9rem;
}

.pg-empty { padding: 1.5rem; text-align: center; color: var(--fs-muted); }

@media (max-width: 575.98px) {
    .pg-modal { padding: 0; }

    .pg-dialog {
        width: 100%;
        height: 100%;
        max-height: 100%;
        border-radius: 0;
    }
}

@media (max-width: 991.98px) {
    /* Narrow: the figures and the campuses go, because the wording and what you
       can do to it are what a phone is for here. */
    .pg-table { table-layout: auto; }

    .pg-col-kind,
    .pg-col-campus,
    .pg-item > td:nth-child(3),
    .pg-item > td:nth-child(4) { display: none; }

    .pg-fields { grid-template-columns: minmax(0, 1fr); }
}

/* --- Evidence carried into the follow-up -------------------------------------

    The same list, quieter, and with no Remove beside it: these links belong to the
    baseline and to the growth plan, and this is a copy of the record rather than
    the record. Set apart so nobody mistakes it for the box they are filling in.
*/
.lp-links--carried {
    margin-top: .6rem;
    padding-top: .6rem;
    border-top: 1px dashed var(--fs-line);
}

.lp-links--carried .lp-links__label { color: var(--fs-muted); }

.lp-links__source {
    margin: .4rem 0 .15rem;
    font-size: .68rem;
    font-weight: 700;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--fs-muted);
}

.lp-links--carried .lp-links__item { font-size: .84rem; }

/* --- Waiting on somebody else ------------------------------------------------

    Sits where the button was, so the eye lands on the answer to "what now?" in the
    same place it last found something to press. Stated, not styled as a warning:
    waiting for an approval is the normal course of the year, not a problem.
*/
.lp-foot--waiting { align-items: center; }

.lp-waiting {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    font-size: .88rem;
    font-weight: 600;
    color: var(--fs-navy);
}

.lp-waiting i { color: var(--fs-muted); }

/* --- Back to the baseline ----------------------------------------------------

    A quiet link rather than a button: it undoes work, so it should be findable
    without competing with the controls that do the work.
*/
.lp-revert { margin: .4rem 0 .2rem; }

.lp-revert__go {
    padding: 0;
    font-size: .8rem;
    color: var(--fs-muted);
    text-decoration: none;
}

.lp-revert__go:hover,
.lp-revert__go:focus-visible { color: var(--fs-navy); text-decoration: underline; }
