/* ==========================================================================
   FranklinOS design system
   --------------------------------------------------------------------------
   References the marketing site (blog.css `.fr2025`) but retunes it for an
   ops tool someone stares at for eight hours:

     - Brand tokens (red, teal, blue, cream) carried over verbatim.
     - Marketing's 20px body / pill buttons / uppercase 800-weight tracking are
       NOT carried over — too shouty and too loose for dense data.
     - Vollkorn is reserved for page titles and the wordmark. Everything a
       dispatcher scans is Libre Franklin.
     - No Bootstrap. The legacy backoffice is on Bootstrap 3 (EOL) behind a
       compatibility shim; there's nothing here worth inheriting.
   ========================================================================== */

:root {
    /* --- Brand (from blog.css) --- */
    --fos-red: #E2412F;           /* Pantone 179 — brand primary */
    --fos-red-dark: #C4341F;
    --fos-red-soft: rgba(226, 65, 47, 0.10);
    --fos-teal: #3EC1CD;
    --fos-teal-dark: #2BA9B5;
    --fos-blue: #435464;
    --fos-blue-dark: #2F3D4A;
    --fos-cream: #EAE3D5;
    --fos-cream-light: #F4EFE6;

    /* --- Surfaces --- */
    --fos-bg: #F4EFE6;            /* warm page field — the brand's own cream-light */
    --fos-surface: #FFFFFF;
    --fos-surface-alt: #FAF8F4;
    /* A row opened for editing. Warmer than `surface-alt`, which is barely off
       white and disappears against a card — this has to say "you are inside
       this one" from across the table, while still letting a white input read
       as an input. It is the page's own cream lifted a little. */
    --fos-open: #F6F2E8;
    --fos-topbar: #2F3D4A;        /* brand blue-dark, not a generic slate */

    /* --- Text --- */
    --fos-text: #2D3A47;
    --fos-text-muted: #6B7682;
    --fos-text-faint: #9BA4AE;
    --fos-text-invert: #FFFFFF;

    /* --- Lines --- */
    --fos-rule: rgba(67, 84, 100, 0.14);
    --fos-rule-strong: rgba(67, 84, 100, 0.28);

    /* --- Status (appointment lifecycle) ---
       Deliberately excludes brand red: red means *emergency* on the board, and
       a status that shares it would be unreadable at a glance.

       Ordered so the day reads by colour: teal is coming, purple is on the
       road, green is happening, and finished work goes quiet. Done being the
       dullest is the point — a board where completed jobs are as loud as live
       ones is a board where the live ones have to be hunted for, and by the
       afternoon most of it is done. */
    /* Sky, not the brand teal. Teal sat 22 units from the dispatched purple
       once both were washed back over white, which is close enough to read as
       one colour — and every blue-violet tested was worse, because they share a
       hue with it. Gold separated best of all and was rejected for meaning
       something else already: it's the inbox's "waiting too long". */
    --fos-scheduled: #0EA5E9;      /* upcoming */
    --fos-dispatched: #7A5AA8;     /* on the way */
    --fos-working: #0F7B5C;        /* on site */
    --fos-done: #6B7682;           /* finished, and out of the way */
    --fos-cancelled: #9BA4AE;

    /* --- Semantic --- */
    --fos-ok: #0F7B5C;
    --fos-ok-bg: rgba(15, 123, 92, 0.10);
    --fos-warn: #B45309;
    --fos-warn-bg: rgba(180, 83, 9, 0.10);
    /* Amber as a row wash, which is not the same job as amber as text. The text
       colour above is picked to be legible when dark on light; laid over a row
       at low alpha it lands in the same pale warm pink as the danger wash —
       seven units apart across all three channels, which is no distance at all.
       This one is pulled toward gold so the two tiers read as two colours. */
    --fos-warn-wash: rgba(214, 158, 0, 0.20);
    --fos-danger: #C4341F;
    --fos-danger-bg: rgba(196, 52, 31, 0.10);

    /* --- Type --- */
    --fos-font-display: 'Vollkorn', Georgia, 'Times New Roman', serif;
    --fos-font: 'Libre Franklin', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    --fos-font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;

    /* --- Shape --- */
    --fos-radius-sm: 6px;
    --fos-radius: 10px;
    --fos-radius-lg: 14px;
    --fos-shadow: 0 1px 2px rgba(45, 58, 71, 0.06), 0 8px 24px rgba(45, 58, 71, 0.08);
    --fos-shadow-sm: 0 1px 2px rgba(45, 58, 71, 0.08);
    --fos-ease: 0.18s cubic-bezier(0.4, 0, 0.2, 1);

    /* --- Space (4px grid — the marketing site has no scale; this is an upgrade) --- */
    --fos-s1: 4px;  --fos-s2: 8px;  --fos-s3: 12px; --fos-s4: 16px;
    --fos-s5: 20px; --fos-s6: 24px; --fos-s8: 32px; --fos-s10: 40px;
}

/* --- Reset ---------------------------------------------------------------- */

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

body {
    margin: 0;
    background: var(--fos-bg);
    color: var(--fos-text);
    font-family: var(--fos-font);
    font-size: 14px;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
}

a { color: var(--fos-blue); text-decoration: none; }
a:hover { color: var(--fos-red); text-decoration: underline; }

/* Teal focus ring — carried verbatim from the marketing site. */
:focus-visible {
    outline: 3px solid var(--fos-teal);
    outline-offset: 2px;
    border-radius: var(--fos-radius-sm);
}

/* --- Wordmark ------------------------------------------------------------- */

.fos-wordmark {
    font-family: var(--fos-font);
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1;
    white-space: nowrap;
}
/* The logo images. Sized by height so the intrinsic width just follows — the
   lockup's aspect ratio is the designer's business, not the layout's. The
   .fos-wordmark rules below still render the same lockup in CSS text, and are
   kept as the type-only fallback (and for anywhere an image would be overkill). */
.fos-logo { display: block; width: auto; max-width: 100%; }
.fos-logo--bar { height: 16px; }
/* This lockup is ~7.4:1, so height drives a lot of width: 52px ran 386px and
   overhung the 380px sign-in card. 40px lands at ~297px — clearly inside the
   card, which is what makes it read as deliberate rather than crammed. */
.fos-logo--login { height: 36px; margin: 0 auto; }

.fos-wordmark__degree { color: var(--fos-red); }
.fos-wordmark__name { color: var(--fos-red); }
.fos-wordmark__os { color: var(--fos-blue); margin-left: 0.12em; }

.fos-wordmark--invert .fos-wordmark__name,
.fos-wordmark--invert .fos-wordmark__degree { color: #fff; }
.fos-wordmark--invert .fos-wordmark__os { color: var(--fos-red); }

/* --- Topbar --------------------------------------------------------------- */

.fos-topbar {
    background: var(--fos-topbar);
    position: sticky;
    top: 0;
    z-index: 100;
}
/* 3px brand stripe — the marketing header's signature detail. */
.fos-topbar::after {
    content: '';
    display: block;
    height: 3px;
    background: linear-gradient(to right, var(--fos-red), var(--fos-red-dark) 60%, var(--fos-red));
}
.fos-topbar__inner {
    max-width: 1440px;
    margin: 0 auto;
    padding: 0 var(--fos-s6);
    height: 56px;
    display: flex;
    align-items: center;
    gap: var(--fos-s6);
}
.fos-topbar__brand { font-size: 19px; }
.fos-topbar__brand:hover { text-decoration: none; }

/* --- Nav search ----------------------------------------------------------- */

.fos-search {
    position: relative;
    /* Trimmed ~15% (was 340/420). The bar was wider than anything typed into
       it — a job number, a surname, a phone — and the room reads better as
       breathing space around the section links, which is what people are
       actually aiming at. */
    flex: 1 1 290px;
    max-width: 357px;
    margin-left: var(--fos-s2);
}
.fos-search input[type='search'] {
    width: 100%;
    padding: 7px 12px 7px 32px;
    font-size: 13.5px;
    color: #fff;
    background: rgba(255, 255, 255, 0.10)
        /* magnifier, inlined so the topbar needs no icon system */
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239BA4AE' stroke-width='2.5' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M20 20l-4-4'/%3E%3C/svg%3E")
        no-repeat 9px center / 14px;
    border: 1.5px solid transparent;
    border-radius: var(--fos-radius-sm);
    transition: background var(--fos-ease), border-color var(--fos-ease);
}
.fos-search input[type='search']::placeholder { color: rgba(255, 255, 255, 0.45); }
.fos-search input[type='search']:hover { background-color: rgba(255, 255, 255, 0.14); }
.fos-search input[type='search']:focus {
    outline: none;
    background-color: #fff;
    color: var(--fos-text);
    border-color: var(--fos-teal);
    box-shadow: 0 0 0 3px rgba(62, 193, 205, 0.25);
}
.fos-search input[type='search']:focus::placeholder { color: var(--fos-text-faint); }
.fos-search input::-webkit-search-cancel-button { -webkit-appearance: none; }

.fos-search__panel {
    position: absolute;
    top: calc(100% + 7px);
    left: 0;
    right: 0;
    max-height: 440px;
    overflow-y: auto;
    background: var(--fos-surface);
    border: 1px solid var(--fos-rule);
    border-radius: var(--fos-radius);
    box-shadow: 0 12px 32px rgba(45, 58, 71, 0.22);
    z-index: 200;
    padding: var(--fos-s1) 0;
}
.fos-search__group {
    display: flex;
    justify-content: space-between;
    padding: var(--fos-s2) var(--fos-s3) 5px;
    font-size: 10.5px;
    font-weight: 800;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--fos-text-faint);
}
.fos-search__hit {
    display: flex;
    align-items: center;
    gap: var(--fos-s2);
    padding: 7px var(--fos-s3);
    text-decoration: none;
    color: var(--fos-text);
}
.fos-search__hit:hover,
.fos-search__hit.is-active {
    background: var(--fos-teal-soft, rgba(62, 193, 205, 0.12));
    text-decoration: none;
}
.fos-search__hit-main { min-width: 0; flex: 1 1 auto; }
.fos-search__hit-title {
    font-size: 13.5px;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.fos-search__hit-sub {
    font-size: 12px;
    color: var(--fos-text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.fos-search__all,
.fos-search__empty {
    display: block;
    padding: var(--fos-s3);
    font-size: 12.5px;
    text-align: center;
    color: var(--fos-text-muted);
}
.fos-search__all {
    border-top: 1px solid var(--fos-rule);
    margin-top: var(--fos-s1);
    font-weight: 600;
    color: var(--fos-blue);
}
.fos-search__all:hover,
.fos-search__all.is-active { background: var(--fos-surface-alt); text-decoration: none; }
.fos-search__empty { font-style: italic; color: var(--fos-text-faint); }

.fos-nav { display: flex; gap: var(--fos-s1); margin-left: auto; align-items: center; }
/* Sign out has to be a <button> in a POST form (Django 5+ rejects GET logout),
   so it's styled identically to the links — the nav shouldn't leak that detail. */
.fos-nav a,
.fos-nav__logout button {
    color: rgba(255, 255, 255, 0.75);
    padding: 7px var(--fos-s3);
    border-radius: var(--fos-radius-sm);
    font-weight: 500;
    font-size: 13.5px;
    /* Every item on one line. "Sign out" is the only two-word one and it broke
       across two, which pushed the bar taller than the rest of the nav. */
    white-space: nowrap;
    transition: background var(--fos-ease), color var(--fos-ease);
}
.fos-nav a:hover,
.fos-nav__logout button:hover { background: rgba(255, 255, 255, 0.10); color: #fff; text-decoration: none; }
.fos-nav a.is-active { background: rgba(255, 255, 255, 0.14); color: #fff; }

.fos-nav__logout { display: flex; margin: 0; }
.fos-nav__logout button {
    background: none;
    border: 0;
    font-family: var(--fos-font);
    cursor: pointer;
    line-height: inherit;
}
.fos-nav__sep { width: 1px; height: 20px; background: rgba(255,255,255,.18); margin: 0 var(--fos-s2); }

/* --- Layout --------------------------------------------------------------- */

.fos-container {
    max-width: 1440px;
    margin: 0 auto;
    padding: var(--fos-s6) var(--fos-s6) var(--fos-s10);
}
.fos-container--narrow { max-width: 760px; }

.fos-page-head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--fos-s4);
    margin-bottom: var(--fos-s5);
    flex-wrap: wrap;
}
.fos-page-head__title {
    font-family: var(--fos-font-display);
    font-weight: 600;
    font-size: 30px;
    line-height: 1.15;
    letter-spacing: -0.01em;
    margin: 0;
}
.fos-page-head__sub { margin: var(--fos-s1) 0 0; color: var(--fos-text-muted); font-size: 14px; }
/* Links in a subheading sit on muted grey, where the ordinary link colour is
   only a shade darker and reads as emphasis rather than something to click —
   hover tells the truth, but only once you've already guessed. Weight is what
   separates them at rest. Applies to every breadcrumb of this shape: job →
   customer, request → Customer Service, booking → account. */
.fos-page-head__sub a { font-weight: 600; }

/* A single-task screen — booking a call, adding a customer — reads better as a
   centered column than pinned to the left edge of a wide window. Wraps the page
   head too, so the heading stays aligned with the form under it. Text stays
   left-aligned inside: centered labels and inputs are harder to scan. */
.fos-narrow { max-width: 680px; margin-inline: auto; }

/* Site footer. Under every signed-in page, so it stays quiet: muted, small, and
   separated by a rule rather than a slab of color. */
.fos-foot {
    /* Matches .fos-container's width and side padding so the rule lines up with
       the content above it rather than floating at a different width. */
    max-width: 1440px;
    margin: 0 auto;
    padding: var(--fos-s5) var(--fos-s6) var(--fos-s8);
    border-top: 1px solid var(--fos-rule);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--fos-s3);
    flex-wrap: wrap;
    font-size: 13px;
    color: var(--fos-text-muted);
}
.fos-foot__mark { font-weight: 600; }
.fos-foot a { color: var(--fos-text-muted); font-weight: 600; }
.fos-foot a:hover { color: var(--fos-text); text-decoration: underline; }

@media print { .fos-foot { display: none !important; } }

/* Segmented control — a small fixed set of mutually exclusive filters, where a
   <select> would hide the options behind a click. Shared: the dispatch board
   filters markets with it, the inbox switches between Unclaimed / Mine / Done. */
.fos-segmented {
    display: inline-flex;
    background: var(--fos-surface);
    border: 1px solid var(--fos-rule-strong);
    border-radius: var(--fos-radius-sm);
    margin-right: var(--fos-s2);
    /* Scrolls inside itself rather than dragging the page. Seven filters is
       443px, which on a 375px phone made the whole document scroll sideways —
       the header, the cards and the table all shifted, and the logo went off
       the left edge. `overflow: hidden` was worse than useless here: it clipped
       the control without stopping it being too wide. */
    max-width: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}
.fos-segmented::-webkit-scrollbar { display: none; }
.fos-segmented a {
    /* Never squeezed to fit — a filter reading "Needs\u2026" is worse than one
       you scroll to. */
    flex: 0 0 auto;
    white-space: nowrap;
    padding: 8px 13px;
    font-size: 13px;
    font-weight: 700;
    color: var(--fos-text-muted);
    border-right: 1px solid var(--fos-rule);
    transition: background var(--fos-ease), color var(--fos-ease);
}
.fos-segmented a:last-child { border-right: 0; }
.fos-segmented a:hover { background: var(--fos-surface-alt); text-decoration: none; color: var(--fos-text); }
.fos-segmented a.is-active { background: var(--fos-blue-dark); color: #fff; }

/* --- Cards ---------------------------------------------------------------- */

.fos-card {
    background: var(--fos-surface);
    border: 1px solid var(--fos-rule);
    border-radius: var(--fos-radius-lg);
    box-shadow: var(--fos-shadow);
    padding: var(--fos-s5);
}
/* Doubled class deliberately. A modifier has to beat the base rule wherever it
   appears, and this one didn't: the mobile `.fos-card { padding: 16px }` further
   down the file has the same specificity and simply came later, so on a phone
   flush cards quietly stopped being flush. Their headers then paid the card's
   padding *and* their own, which is why an invoice title sat sixteen pixels
   further in than the one above it. */
.fos-card.fos-card--flush { padding: 0; overflow: hidden; }
/* The header of a flush card — a title, and often a badge or a button beside it.
   A class rather than the inline padding this replaces in seven templates:
   inline styles can't be answered by a media query, so on a phone these headers
   held the desktop 20px while every ordinary card dropped to 16px, and the
   titles sat four pixels further in than the ones above and below them.

   Wraps, so a long title and a button stop competing for the same line on a
   narrow screen — "Appointments · 1" and "Add appointment" were squeezing each
   other. */
.fos-card__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--fos-s3);
    padding: var(--fos-s5) var(--fos-s5) 0;
}
.fos-card__title {
    font-size: 12px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--fos-text-muted);
    padding-bottom: var(--fos-s3);
    margin-bottom: var(--fos-s4);
    border-bottom: 1px solid var(--fos-rule);
}
/* A second heading inside a card, for a card that holds two things somebody
   wrote at different times. The card title's eyebrow without its rule: the
   rule is what separates a card from the page, and drawing a second one down
   the middle makes one card read as two. */
.fos-card__subtitle {
    font-size: 12px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--fos-text-muted);
    margin: var(--fos-s5) 0 var(--fos-s2);
}
.fos-card__subtitle:first-child { margin-top: 0; }

.fos-card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: var(--fos-s5);
}

/* Uniform cards: every card keeps the width it has when a row is full, so a
   section with a single card doesn't stretch it across the whole row. auto-fill
   (not auto-fit) preserves the empty tracks that keep the lone card card-sized —
   used on Settings, where sections vary from one card to four. */
.fos-card-grid--uniform {
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}

/* --- Two-column layouts --------------------------------------------------- */
/* These live here rather than in a style="" attribute on each page for one
   reason: an inline grid-template-columns cannot be undone by a media query, so
   every page that hard-coded its columns stayed two-up on a phone and shoved its
   sidebar off the screen. A class can collapse; an attribute can't. */

/* Main content with a sidebar beside it — job and customer detail. Collapses
   before the 340px rail gets too narrow to read, not at some phone width. */
.fos-split {
    display: grid;
    /* 400, not 340. The side column carries the SMS thread now, and a
       conversation at 340px wrapped every message to three or four lines. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 400px);
    gap: var(--fos-s5);
    align-items: start;
}

/* The space between cards belongs to the column, not to each card.
   It used to be an inline margin-bottom on every card, which meant the LAST
   card in a column carried none — correct, until the cards were reordered and
   whichever one had been last butted straight into its new neighbour with no
   gap at all. Reading as one merged block. A gap here cannot be got wrong by
   moving things around. */
/* The strip under a job heading: when is the visit, who is going, what is
   owed. One row of facts rather than four cards, because they are read
   together and in a glance — and because four cards for four numbers is how a
   page stops having a hierarchy at all.

   Divided by rules rather than gaps so it reads as one object. It wraps to two
   across on a narrow window and the rules follow, which is why the border sits
   on the cell and is stripped from the last in each row by the nth-child
   below rather than being drawn between them. */
.fos-strip {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    background: var(--fos-surface);
    border: 1px solid var(--fos-rule);
    border-radius: var(--fos-radius);
    margin-bottom: var(--fos-s5);
}
.fos-strip > div {
    padding: var(--fos-s3) var(--fos-s4);
    border-right: 1px solid var(--fos-rule);
    min-width: 0;
}
.fos-strip > div:last-child { border-right: 0; }
.fos-strip__label {
    font-size: 10.5px; font-weight: 700; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--fos-text-faint);
    margin-bottom: 2px;
}
.fos-strip__value { font-size: 15px; font-weight: 600; line-height: 1.3; }
.fos-strip__value--none { color: var(--fos-text-faint); font-weight: 500; }
.fos-strip__sub { font-size: 12px; color: var(--fos-text-muted); margin-top: 1px; }
.fos-strip__sub--settled { color: var(--fos-ok); }
.fos-strip__sub--owing { color: var(--fos-warn); font-weight: 600; }
.fos-num { font-variant-numeric: tabular-nums; }

/* No sticky side column. It was tried and taken out.

   Pinning it meant the column stopped scrolling with the page, and when its
   own content was taller than the window it grew a second scrollbar inside the
   first — so reaching the bottom of the text thread meant scrolling a panel
   inside a page that was itself scrolling. A page that scrolls in one
   direction, once, is worth more than a column that follows you down it. */

.fos-split > div {
    display: flex;
    flex-direction: column;
    gap: var(--fos-s5);
    min-width: 0;
}

/* And neutralise the per-card margins the gap now replaces.
   !important because several of these are inline styles, and inline is exactly
   what this has to beat. The shared partials — notes, files, the invoice panel —
   carry their own margin and are used on pages that have no such column, so
   they cannot simply have it removed; overriding it here is narrower than
   editing them and safer than leaving the two to add up, which showed as a
   double gap under half the cards. */
.fos-split > div > * { margin-bottom: 0 !important; }

/* A side column carrying tables rather than a conversation. The shared 400px is
   sized for the SMS thread; the invoice view puts the cost breakdown there, and
   a five-column table (SKU, description, qty, unit cost, cost) wants 560px —
   at 400 it scrolled sideways inside its own card and showed three columns of
   five, so the unit cost you went looking for was the one off-screen. 580 and
   not 560, because the card's own border ate the last two pixels and the table
   went on scrolling, and because a long description needs somewhere to go.

   Stacks earlier than the plain split, too: this variant's left column holds the
   invoice document, which reads badly under about 650px, and at a 1000px window
   the 1fr side of a 580px pair is 360. */
.fos-split--wide { grid-template-columns: minmax(0, 1fr) minmax(0, 580px); }

/* Two fields sharing a row on a form. */
.fos-pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--fos-s4);
}

@media (max-width: 900px) {
    .fos-split { grid-template-columns: minmax(0, 1fr); }
}
/* Named explicitly rather than left to inherit the rule above: same specificity,
   and `.fos-split--wide` is declared after it, so without this the modifier
   would win at every width and the two columns would never stack. */
@media (max-width: 1100px) {
    .fos-split--wide { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 700px) {
    .fos-pair { grid-template-columns: minmax(0, 1fr); }
}

/* --- Stats ---------------------------------------------------------------- */

.fos-stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: var(--fos-s4);
    margin-bottom: var(--fos-s5);
}
.fos-stat {
    background: var(--fos-surface);
    border: 1px solid var(--fos-rule);
    border-radius: var(--fos-radius);
    padding: var(--fos-s4);
}
.fos-stat__label {
    font-size: 11.5px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--fos-text-muted);
}
.fos-stat__value {
    /* Shrinks rather than spills. A year's sales is eight figures wide and, at
       a fixed 28px in a 160px card, "$8,455,756.36" ran out of the box and read
       as $8,455,756.2 — a clipped number is worse than a small one, because it
       looks like a number. clamp() keeps the usual size on anything roomy and
       gives way only where it must. */
    font-size: clamp(20px, 2.1vw, 28px);
    font-weight: 700;
    line-height: 1.1;
    margin-top: var(--fos-s1);
    font-variant-numeric: tabular-nums;
    /* And if even the floor doesn't fit, wrap — never cut. */
    overflow-wrap: anywhere;
}

/* --- Buttons -------------------------------------------------------------- */
/* Marketing uses 999px pills, uppercase, 800 weight, 0.12em tracking, and a
   red glow. That's a landing-page CTA, not something you click 200x a day. */

.fos-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--fos-s2);
    border: 1px solid var(--fos-rule-strong);
    background: var(--fos-surface);
    color: var(--fos-text);
    border-radius: var(--fos-radius-sm);
    padding: 8px 14px;
    font-family: var(--fos-font);
    font-size: 13.5px;
    /* Stated, because otherwise it is not the same on both kinds of button.
       A <button> takes `normal` from the browser's own sheet; an <a> inherits
       the page's 1.5. Same class, same padding, same text — and a link-button
       came out 38.3px against a real button's 34, everywhere the two sat side
       by side. 1.2 lands on the height buttons already had. */
    line-height: 1.2;
    font-weight: 600;
    cursor: pointer;
    text-decoration: none;
    transition: background var(--fos-ease), border-color var(--fos-ease), box-shadow var(--fos-ease);
    /* A button never wraps. "Answers" broke to two lines in a narrow
       table cell and read as "Answ ers". */
    white-space: nowrap;
}
.fos-btn:hover { background: var(--fos-surface-alt); text-decoration: none; color: var(--fos-text); }
.fos-btn--primary {
    background: var(--fos-red);
    border-color: var(--fos-red);
    color: #fff;
    box-shadow: var(--fos-shadow-sm);
}
.fos-btn--primary:hover { background: var(--fos-red-dark); border-color: var(--fos-red-dark); color: #fff; }
.fos-btn--block { width: 100%; }
.fos-btn--lg { padding: 11px 18px; font-size: 15px; }
/* `hidden` means hidden.

   The browser's own rule is `[hidden] { display: none }`, which is the weakest
   kind of rule there is — every class here that sets a display beats it. So a
   button with `hidden = true` sat there looking like a button: the rules panel
   hid the whole condition half of a rule that watches nothing, and "+ Add a
   condition" stayed on screen, appending rows into a container nobody could
   see. It read as a button that did not work.

   `!important` because the point is to outrank whatever any class says about
   display. Anything wanting to be visible should not be marked hidden. */
[hidden] { display: none !important; }

.fos-btn--sm { padding: 5px 10px; font-size: 12.5px; }
.fos-btn[disabled] { opacity: 0.5; cursor: not-allowed; }
.fos-btn--danger { color: var(--fos-danger); border-color: var(--fos-danger); background: var(--fos-danger-bg); }
.fos-btn--danger:hover { color: #fff; background: var(--fos-danger); border-color: var(--fos-danger); }
.fos-btn--warn { color: var(--fos-warn); border-color: var(--fos-warn); background: var(--fos-warn-bg); }
.fos-btn--warn:hover { color: #fff; background: var(--fos-warn); border-color: var(--fos-warn); }

/* Cancel-a-job, folded behind a click so the button row isn't a minefield. The
   summary is styled as the button; the form drops below it when opened. */
.fos-cancel { display: inline-block; position: relative; }
.fos-cancel > summary { list-style: none; cursor: pointer; }
.fos-cancel > summary::-webkit-details-marker { display: none; }
.fos-cancel__form {
    position: absolute; z-index: 20; margin-top: var(--fos-s2); right: 0;
    width: max(260px, 100%); padding: var(--fos-s3);
    background: var(--fos-surface); border: 1px solid var(--fos-rule);
    border-radius: var(--fos-radius); box-shadow: var(--fos-shadow);
    display: flex; flex-direction: column; gap: var(--fos-s2);
}

/* A "More …" menu on a page head.
 *
 * Built on <details>, like the cancel popover above it: Escape closes it, a
 * click outside closes it, and it works with scripting off. A page that had
 * nine buttons in a row now has three and this — the rare and the dangerous
 * behind one press, rather than the same size and colour as the thing somebody
 * came to do.
 */
.fos-menu { display: inline-block; position: relative; }
.fos-menu > summary { list-style: none; cursor: pointer; }
.fos-menu > summary::-webkit-details-marker { display: none; }
/* Stated rather than left to the browser. A closed <details> hides its own
   content, but the panel sets display:flex, and a child that declares its own
   display can win that argument in some engines — a menu that quietly renders
   open on one browser is not a thing to find out about later. */
.fos-menu:not([open]) .fos-menu__panel { display: none; }
.fos-menu__panel {
    position: absolute; z-index: 30; right: 0; margin-top: var(--fos-s2);
    min-width: 260px; padding: var(--fos-s2);
    background: var(--fos-surface); border: 1px solid var(--fos-rule);
    border-radius: var(--fos-radius); box-shadow: var(--fos-shadow);
    display: flex; flex-direction: column; gap: 2px;
}
/* Every entry reads as one row whether it is a link or a form's button, so the
   menu does not look like two kinds of thing stacked. */
.fos-menu__panel > a,
.fos-menu__panel button[type="submit"]:not(.fos-btn) {
    display: block; width: 100%; text-align: left;
    padding: 8px 10px; border: 0; border-radius: 6px;
    background: none; color: var(--fos-text); cursor: pointer;
    font: inherit; font-size: 13.5px; text-decoration: none;
}
.fos-menu__panel > a:hover,
.fos-menu__panel button[type="submit"]:not(.fos-btn):hover { background: var(--fos-bg); }
.fos-menu__panel form { margin: 0; }
/* Cancelling is last, separated, and still asks for a reason before it does
   anything — the menu hides it from the row, not from the thinking. */
.fos-menu__danger {
    margin-top: var(--fos-s2); padding: var(--fos-s2) 10px 4px;
    border-top: 1px solid var(--fos-rule);
    display: flex; flex-direction: column; gap: var(--fos-s2);
}

/* Booking card header: the section title on the left, the "dig deeper" links
   (customer, selected address) as small buttons on the right — anchored to the
   card, opened in a new tab so the half-filled form is never lost underneath. */
.fos-card__head {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--fos-s3); flex-wrap: wrap;
    padding-bottom: var(--fos-s3); margin-bottom: var(--fos-s4);
    border-bottom: 1px solid var(--fos-rule);
}
.fos-card__title--flush { padding: 0; margin: 0; border: 0; }
/* The head normally sits at the top edge of a `.fos-card--flush` — an unpadded
   card — and supplies that padding itself. Inside an ordinary padded card the
   two stack, and the title ends up indented a full step further than every
   field beneath it, which is what made the booking form's "The work" look like
   it belonged to something else. This variant keeps the rule and drops the
   padding the flush case needs. */
/* Doubled, for the same reason .fos-card--flush is: the mobile
   `.fos-card__head { padding: ... }` further down the file has identical
   specificity and simply comes later, so on a phone the indent this exists to
   remove would come straight back. */
.fos-card__head--inset.fos-card__head--inset { padding: 0 0 var(--fos-s3); }
.fos-diglinks { display: flex; gap: var(--fos-s2); flex-wrap: wrap; }

/* A required answer left blank, caught before the booking submits so the page
   never bounces back to the top with the error hidden mid-form. */
.fos-field.is-missing select,
.fos-field.is-missing input,
.fos-field.is-missing textarea { border-color: var(--fos-danger); }
#slot-picker.is-missing { outline: 2px solid var(--fos-danger); outline-offset: 4px; border-radius: var(--fos-radius); }

/* A plain text action inside a table cell — reads as a link, not a button. */
.fos-link { color: var(--fos-blue); font-weight: 600; font-size: 13px; }
.fos-link:hover { text-decoration: underline; }

/* --- Badges --------------------------------------------------------------- */
/* Pills belong here — small, semantic, scanned not clicked. */

/* A value you can take back off — a postal code on a market, for now. Reads as
   data rather than as a control until you're over it, because a row of buttons
   would look like a row of things to press and these are mostly there to be
   read. The cross only earns its contrast on hover. */
.fos-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    border: 1px solid var(--fos-rule);
    background: var(--fos-surface);
    border-radius: 999px;
    padding: 3px 10px;
    font-size: 12.5px;
    font-variant-numeric: tabular-nums;
    color: var(--fos-text);
    cursor: pointer;
}
.fos-chip > span[aria-hidden] { color: var(--fos-text-muted); font-size: 13px; line-height: 1; }
.fos-chip:hover { border-color: var(--fos-red); color: var(--fos-red); }
.fos-chip:hover > span[aria-hidden] { color: var(--fos-red); }

.fos-badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    border-radius: 999px;
    padding: 2px 9px;
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: 0.02em;
    white-space: nowrap;
    background: rgba(107, 118, 130, 0.12);
    color: var(--fos-text-muted);
}
.fos-badge--scheduled  { background: rgba(14, 165, 233, 0.16);  color: #0369A1; }
.fos-badge--dispatched { background: rgba(122, 90, 168, 0.14);  color: var(--fos-dispatched); }
.fos-badge--working    { background: rgba(15, 123, 92, 0.14);   color: var(--fos-working); }
/* Neutral, not the green "ok" fill — that pairing was tied to done being green
   and would now put grey text on a green wash. */
.fos-badge--done       { background: rgba(107, 118, 130, 0.12); color: var(--fos-done); }
.fos-badge--cancelled  { background: rgba(155, 164, 174, 0.14); color: var(--fos-cancelled); }
/* SMS thread. Bubbles rather than a table because it's a conversation, and a
   CSR scanning it needs to see at a glance who said what — theirs on the left,
   ours on the right, the way every phone shows it. */
.fos-thread {
    display: flex; flex-direction: column; gap: var(--fos-s2);
    max-height: 340px; overflow-y: auto;
    /* A gutter on the right for the scrollbar to live in. Without it an
       outbound bubble — which is right-aligned, and on the email thread runs to
       92% — ends exactly where the scrollbar starts, so the two touch and the
       bubble looks clipped by it. */
    padding: var(--fos-s2) var(--fos-s3) var(--fos-s2) 0;
    scrollbar-gutter: stable;
    /* Stop the scroll chaining to the page when you reach either end. Without
       it, reading to the bottom of a conversation hands the wheel to the
       document mid-gesture, which is most of what "not smooth" means here. */
    overscroll-behavior: contain;
    /* Where the conversation ends. It used to finish flush against whatever
       came next — zero pixels between the thread and the template picker — so a
       scrolling thread cut its last bubble off exactly on the picker's top
       edge, and the two read as one overlapping the other. A rule says the log
       has ended; the space keeps the controls off it. */
    border-bottom: 1px solid var(--fos-rule);
    margin-bottom: var(--fos-s3);
}
.fos-msg { max-width: 78%; }
.fos-msg__body {
    padding: 8px 12px;
    border-radius: var(--fos-radius-lg);
    font-size: 14px; line-height: 1.45;
    white-space: pre-wrap; word-break: break-word;
}
.fos-msg__meta { font-size: 11.5px; color: var(--fos-text-faint); margin-top: 3px; }

.fos-msg--in  { align-self: flex-start; }
.fos-msg--in .fos-msg__body {
    background: var(--fos-surface-alt);
    border: 1px solid var(--fos-rule);
    border-bottom-left-radius: var(--fos-radius-sm);
}
.fos-msg--out { align-self: flex-end; }
.fos-msg--out .fos-msg__body {
    background: var(--fos-blue-dark); color: #fff;
    border-bottom-right-radius: var(--fos-radius-sm);
}
.fos-msg--out .fos-msg__meta { text-align: right; }
/* A text still inside its half minute. Faded rather than styled as something
   else: it is the message that is about to go, not a different kind of thing,
   and it has to read as "nearly gone" rather than as an error. */
.fos-msg.is-holding .fos-msg__body { opacity: 0.62; }
.fos-msg__hold {
    display: flex; align-items: center; gap: var(--fos-s2);
    justify-content: flex-end;
    font-size: 11.5px; color: var(--fos-text-faint); margin-top: 3px;
}
/* The one control on a held text. Shaped like the × that takes a file off a
   record, because it is the same gesture and should not need learning twice —
   what it opens is the text again, where clearing the box withdraws it. */
.fos-msg__x {
    flex: 0 0 auto;
    border: 0;
    background: none;
    color: var(--fos-text-faint);
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
    padding: 0 3px;
    border-radius: var(--fos-radius-sm);
}
.fos-msg__x:hover { color: var(--fos-danger); background: var(--fos-danger-bg); }

/* A text that never left has to look wrong, not just say so in small print. */
.fos-msg.is-failed .fos-msg__body {
    background: var(--fos-danger-bg); color: var(--fos-danger);
    border: 1px dashed var(--fos-danger);
}

/* A picture in a text. Capped rather than full-width: the thread is a column of
   bubbles and a tall photo shouldn't push the conversation off the screen. */
.fos-msg__media {
    display: block;
    /* The bubble is a shrink-to-fit flex item, so an image with no intrinsic
       size yet collapses to ~2px and then jumps on decode — 257px of shift,
       measured, right under the reader. The template sets an explicit inline
       width (MessageMedia.thread_box) to hold the space; height stays auto so
       aspect-ratio drives it. max-height here must stay in step with
       THREAD_MAX_HEIGHT in inbox/models.py. */
    height: auto;
    max-width: 100%;
    max-height: 260px;
    border-radius: var(--fos-radius-lg);
    border: 1px solid var(--fos-rule);
    margin-bottom: 4px;
    background: var(--fos-surface-alt);
    cursor: zoom-in;
}
video.fos-msg__media { cursor: default; }

/* Email reply: a composer, not a text box. Stacked so the message gets the full
   width and can be dragged taller — a CSR writing three paragraphs through a
   three-line slot is how you get one-line answers. */
.fos-mail-reply { display: flex; flex-direction: column; gap: var(--fos-s3); }
.fos-mail-reply textarea {
    width: 100%;
    min-height: 150px;
    resize: vertical;
    font-family: var(--fos-font);
    line-height: 1.5;
}
.fos-mail-reply__foot {
    display: flex; align-items: center; gap: var(--fos-s3); flex-wrap: wrap;
}
.fos-mail-reply__sig {
    border-left: 2px solid var(--fos-rule);
    padding-left: var(--fos-s3);
}
.fos-mail-reply__sig pre {
    margin: 2px 0 0;
    font-family: var(--fos-font);
    font-size: 12.5px;
    color: var(--fos-text-muted);
    white-space: pre-wrap;
}
/* The signature on a sent message: present, quiet, not competing with what the
   CSR actually wrote. */
.fos-msg__sig {
    margin-top: 4px;
    padding: 6px 12px;
    font-size: 12px;
    color: var(--fos-text-faint);
    white-space: pre-wrap;
    border-left: 2px solid var(--fos-rule);
}

/* A request Halo is holding — queued for her, being worked, or answered by her.
   Not handed_off: she passed that one back, so it's ordinary CSR work.

   Tinted and rule-marked rather than faded. Opacity was the first attempt and
   it's the wrong tool: it makes the text harder to read at exactly the moment
   somebody is scanning the queue, and it punishes the reader for a state that
   isn't their fault. A cool tint plus a teal edge says "different mode" while
   leaving every word at full contrast.

   Still fully clickable and still claimable. Disabling the row would mean a CSR
   who wanted to step in couldn't. */
.fos-table tr.is-ai-held > td {
    /* 0.055 was the first value here and it was a mistake: it shifts white by
       17 of a possible 765, which is invisible on most monitors. A "greyed out"
       row that nobody can see is worse than none, because the code claims the
       CSR was told. */
    background: rgba(62, 193, 205, 0.16);
    color: var(--fos-text-muted);
}
.fos-table tr.is-ai-held > td:first-child {
    box-shadow: inset 4px 0 0 var(--fos-teal);
}
.fos-table tr.is-ai-held:hover > td { background: rgba(62, 193, 205, 0.24); }
/* Links and the waiting clock stay at full strength — they're what you scan for. */
.fos-table tr.is-ai-held a { color: var(--fos-blue); }
.fos-badge--ai {
    background: rgba(62, 193, 205, 0.14);
    color: var(--fos-teal-dark);
}
/* What the assistant did, on the request page. Teal rather than the status
   palette: this is a different kind of actor, and it shouldn't be mistaken for
   an appointment state. */
.fos-ai-card { border-left: 3px solid var(--fos-teal); }
.fos-ai-note {
    padding-bottom: var(--fos-s3);
    margin-bottom: var(--fos-s3);
    border-bottom: 1px solid var(--fos-rule);
}
.fos-ai-note:last-child { border-bottom: 0; margin-bottom: 0; padding-bottom: 0; }
.fos-ai-draft {
    margin: var(--fos-s2) 0 0;
    padding: 8px 12px;
    background: var(--fos-surface-alt);
    border: 1px solid var(--fos-rule);
    border-radius: var(--fos-radius-sm);
    white-space: pre-wrap;
    font-size: 13px;
}

.fos-close-picks { display: flex; gap: var(--fos-s2); flex-wrap: wrap; }
/* The consequence of closing a request dishonestly. Weightier than help text
   and quieter than an error: an alert would be shouting on a screen where
   nothing has gone wrong yet, and muted grey is what people stop seeing by the
   second week. A left rule in the warning colour, which reads as "this is a
   standing rule" rather than "something is broken". */
.fos-close-warning {
    margin: var(--fos-s4) 0 0;
    padding: var(--fos-s2) 0 var(--fos-s2) var(--fos-s3);
    border-left: 3px solid var(--fos-warn);
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--fos-text-muted);
    max-width: 62ch;
}

/* The message gets a line to itself and the controls sit under it. All three on
   one row left the box 199px of 358 in the side column — you cannot read a
   sentence you have just typed while a paperclip and a Send button take the
   rest of the width. */
.fos-thread__send {
    display: flex; flex-wrap: wrap; gap: var(--fos-s2); align-items: center;
}
.fos-thread__send textarea { flex: 1 0 100%; margin: 0; }
/* The attach control is a <label> wrapping a hidden file input, so it has to be
   told to behave like the button it's dressed as. */
/* margin-right:auto pushes Send to the far edge, so the two controls are not
   sitting against each other under the box. */
.fos-thread__clip { flex: 0 0 auto; cursor: pointer; gap: 4px; margin-right: auto; }
.fos-thread__clip:focus-within { border-color: var(--fos-rule-strong); box-shadow: var(--fos-shadow-sm); }
.fos-thread__clip [data-sms-count]:not(:empty) {
    font-size: 11.5px;
    font-weight: 700;
    color: var(--fos-red);
}
/* The textarea takes the slack; the button keeps its own width and its label
   stays on one line. Without both halves it gets squeezed until "Send text"
   wraps, which in a narrow sidebar is most of the time. */
.fos-thread__send button { flex: 0 0 auto; white-space: nowrap; }

/* Bookable times, offered to the CSR so they never have to read the board.
   Chips rather than a dropdown: the whole value is seeing the choices at once
   and reading two of them down the phone. */
.fos-slots {
    background: var(--fos-surface-alt);
    border: 1px solid var(--fos-rule);
    border-radius: var(--fos-radius-sm);
    padding: var(--fos-s3);
    margin-bottom: var(--fos-s4);
}
.fos-slots__head {
    display: flex; align-items: baseline; gap: var(--fos-s2);
    flex-wrap: wrap; font-size: 13.5px; margin-bottom: var(--fos-s2);
}
.fos-slots__list { display: flex; flex-wrap: wrap; gap: var(--fos-s2); }
.fos-slot {
    display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
    border: 1px solid var(--fos-rule-strong);
    background: var(--fos-surface);
    border-radius: var(--fos-radius-sm);
    padding: 7px 11px;
    font-family: var(--fos-font);
    font-size: 13.5px; font-weight: 600;
    color: var(--fos-text);
    cursor: pointer;
    transition: background var(--fos-ease), border-color var(--fos-ease);
}
.fos-slot:hover { background: var(--fos-surface-alt); }
.fos-slot.is-chosen { background: var(--fos-blue-dark); border-color: var(--fos-blue-dark); color: #fff; }
.fos-slot__who { font-weight: 500; font-size: 11.5px; opacity: 0.75; }

/* Search results for "who is calling". A row rather than a chip, because each
   one carries two actions: pick them, and open their record to decide whether
   it's them at all. Two households on the same street with the same surname are
   indistinguishable in a list. */
/* The caller panel while a pick is being fetched. Dimmed rather than emptied
   so the CSR keeps their place in the list, and inert so an impatient second
   click can't queue a second swap behind the first. */
#booking-panel[aria-busy="true"] { opacity: .55; pointer-events: none; }

.fos-caller-list { display: flex; flex-direction: column; gap: var(--fos-s2); }
.fos-caller {
    display: flex; align-items: stretch; gap: 0;
    border: 1px solid var(--fos-rule-strong);
    border-radius: var(--fos-radius-sm);
    background: var(--fos-surface);
    overflow: hidden;
}
.fos-caller__pick {
    flex: 1 1 auto;
    display: flex; flex-direction: column; gap: 1px;
    padding: 8px 11px;
    font-size: 13.5px; font-weight: 600;
    color: var(--fos-text); text-decoration: none;
}
.fos-caller__pick span { font-weight: 500; font-size: 11.5px; color: var(--fos-text-muted); }
.fos-caller__pick:hover { background: var(--fos-surface-alt); color: var(--fos-text); }
/* Divided off, so it never reads as part of the choosing. Clicking it is a
   detour to look something up, not an answer. */
.fos-caller__view {
    flex: 0 0 auto;
    display: flex; align-items: center;
    padding: 0 12px;
    border-left: 1px solid var(--fos-rule);
    font-size: 12px; font-weight: 600; white-space: nowrap;
    color: var(--fos-text-muted); text-decoration: none;
}
.fos-caller__view:hover { background: var(--fos-surface-alt); color: var(--fos-text); }

/* Asking Halo mid-call. Quiet by default — it is a fallback, not the task — and
   the answer is set apart so a rep can find it at a glance while talking. */
.fos-ask__row { display: flex; gap: var(--fos-s2); }
.fos-ask__row input { flex: 1 1 auto; }
.fos-ask__out {
    margin-top: var(--fos-s3);
    padding: var(--fos-s3);
    border-left: 3px solid var(--fos-teal);
    background: var(--fos-surface-alt);
    border-radius: 0 var(--fos-radius-sm) var(--fos-radius-sm) 0;
    font-size: 14px; line-height: 1.55;
    white-space: pre-wrap;
}
.fos-ask__out--waiting { color: var(--fos-text-muted); font-style: italic; }
/* A refusal is not an answer, and must not look like one. */
.fos-ask__out--refused { border-left-color: var(--fos-warn); color: var(--fos-text-muted); }

/* Adding a caller nobody has on file. Folded away, because most calls are from
   somebody we know and an open address form would suggest the opposite — but
   opened automatically when a search came back empty, which is exactly when it
   is the next thing to do. */
.fos-new-customer { margin-top: var(--fos-s3); }
.fos-new-customer > summary {
    cursor: pointer;
    font-size: 13px; font-weight: 600;
    color: var(--fos-text-muted);
    padding: 6px 0;
}
.fos-new-customer > summary:hover { color: var(--fos-text); }
.fos-new-customer[open] > summary { margin-bottom: var(--fos-s2); }
.fos-new-customer form {
    border-left: 3px solid var(--fos-rule);
    padding-left: var(--fos-s3);
}
/* Past the window this work should be seen in. A dashed edge rather than a
   colour or a dimming: these are perfectly bookable times and the CSR may well
   need them, so this marks them as a different *kind* of answer rather than as
   a worse one. Still fully legible when chosen. */
.fos-slot.is-past-target { border-style: dashed; border-color: var(--fos-warn); }
.fos-slot.is-past-target.is-chosen { border-style: solid; }

/* The call button's form. The button itself wears .board__who-more — see
   board.css — so it is the same 16px control as the "+" beside it. A form is
   block-level by default, which is what put the telephone on a line of its
   own at a third the size. */
.board__who-call { display: flex; }
/* The "+" and the telephone on one line. The gutter is a flex column, so both
   need a row of their own to share — a `display` on either alone blockifies,
   because an inline-flex flex item is a flex item first. */
.board__who-actions { display: flex; align-items: center; gap: 2px; }

/* Inbox rows that have waited too long to be claimed. A stripe on the leading
   edge — inset box-shadow, because a border-left on a <td> disagrees with the
   table's own collapsed borders — plus a wash across the row.

   Both states get the wash. Only overdue used to, which made aging read as an
   ordinary white row: a 3px stripe on the far left of a wide table is not
   something anyone notices, least of all sitting next to rows that are tinted
   red. The whole job of the amber step is to be seen before the red one.

   Neither wash applies to a row the assistant is holding. Row colour answers
   "whose is this" and the stripe answers "how long has it sat" — so a request
   she has in hand stays her teal however long it takes, because the clock
   running on it isn't the CSR's problem to solve. */
.fos-table tr.fos-waiting--aging   > td:first-child { box-shadow: inset 3px 0 0 var(--fos-warn); }
.fos-table tr.fos-waiting--overdue > td:first-child { box-shadow: inset 3px 0 0 var(--fos-danger); }
.fos-table tr.fos-waiting--aging:not(.is-ai-held)   > td { background: var(--fos-warn-wash); }
.fos-table tr.fos-waiting--overdue:not(.is-ai-held) > td { background: var(--fos-danger-bg); }

/* A badge you can click. Same pill as the ones you can't — it is telling you
   the same thing, and re-styling it into a button would have said it was a
   different kind of fact. The cursor and the hover carry that it acts. */
.fos-badge--set {
    border: 0; font-family: inherit; cursor: pointer;
}
.fos-badge--set:hover { filter: brightness(0.94); }
.fos-badge--set:focus-visible {
    outline: 2px solid var(--fos-red); outline-offset: 1px;
}

/* A dialog that asks one question. Narrower than the sheet, which is a list. */
.fos-sheet--ask { width: min(410px, calc(100vw - var(--fos-s6))); }
.fos-sheet--ask .fos-sheet__body { max-height: none; font-size: 14px; }

/* A sentence that has to be read before the button under it is pressed.
   Explicitly not a badge: a badge is `white-space: nowrap` by design, so a
   badge holding a sentence runs off the side of whatever it is in — which is
   how the cancel dialog shipped, with the half that said the invoice would not
   be refunded off the edge of its own box. */
/* A label for somebody using a screen reader and nobody else. For a column of
   buttons whose meaning is obvious by sight and invisible without it. */
.fos-sr-only {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}


/* The notification chips for one contact, together, so the page can swap them
   in place when one is changed. */
.fos-flags { display: inline-flex; gap: 4px; flex-wrap: wrap; }

.fos-note {
    padding: var(--fos-s2) var(--fos-s3);
    border-radius: var(--fos-radius-sm);
    border-left: 3px solid var(--fos-rule-strong);
    background: var(--fos-surface-alt);
    font-size: 13px;
    line-height: 1.45;
}
.fos-note--warn {
    border-left-color: var(--fos-warn);
    background: var(--fos-warn-bg);
}

/* A facts table: label on the left, the answer on the right in the weight that
   says it is the answer. Its own class rather than a rule on `.fos-table`,
   which is also every list of rows on the site — those have no answer column,
   and bolding half of one would be arbitrary.

   The second line under an answer stays light. It is the qualifier — the city
   under the street, who created it under the date — and a card where both
   lines shout has no hierarchy at all. */
.fos-facts td.num { font-weight: 600; }
.fos-facts td.num .fos-faint,
.fos-facts td.num .fos-badge,
.fos-facts td.num .fos-linkish,
.fos-facts td.num .fos-note,
.fos-facts td.num .fos-facts__aside { font-weight: 400; }

.fos-badge--ok    { background: var(--fos-ok-bg);    color: var(--fos-ok); }
.fos-badge--warn  { background: var(--fos-warn-bg);  color: var(--fos-warn); }
.fos-badge--danger{ background: var(--fos-danger-bg);color: var(--fos-danger); }

/* A heading that divides a long settings page into groups. Lived in a <style>
   block inside settings.html until a second page wanted one, at which point a
   page-local class is a class that renders unstyled somewhere. */
.fos-settings-group {
    font-size: 12.5px; font-weight: 600; text-transform: uppercase;
    letter-spacing: .05em; color: var(--fos-text-muted);
    margin: var(--fos-s6) 0 var(--fos-s3); padding-bottom: var(--fos-s2);
    border-bottom: 1px solid var(--fos-rule);
}

/* A setting *of* the checkbox above it rather than another one beside it —
   "email me swap suggestions" under Dispatch, "email me new reviews" under
   Marketing. Indented to sit inside its parent, with a rule down the side.

   A class because the spacing has to be the block's own. As two inline styles
   it inherited whatever gap the parent field happened to have, and the list
   these hang off zeroes the bottom margin of its last row: Marketing is last
   and Dispatch is not, so one nested block sat flush against its label and the
   other an entire field-gap below it. Same markup, same intent, two different
   components to look at. */
.fos-subfield {
    margin: var(--fos-s2) 0 var(--fos-s4) 30px;
    padding-left: var(--fos-s3);
    border-left: 2px solid var(--fos-rule);
}
/* The parent gives up its bottom margin so the gap above a nested block is
   always this block's. Only while the child is actually showing — these are
   [hidden] until their permission is ticked, and a collapsed child must not
   pull the next row up against this one. */
.fos-field:has(+ .fos-subfield:not([hidden])) { margin-bottom: 0; }

/* What the customer receives, shown inside the card that words it and under
   the boxes that do the wording. An inset rather than a card of its own: it is
   the same message as the fields above it, not a second subject, and a panel
   carrying its own shadow inside a card reads as one. */
.fos-preview {
    margin-top: var(--fos-s5);
    padding: var(--fos-s4);
    background: var(--fos-cream-light);
    border: 1px solid var(--fos-rule);
    border-radius: var(--fos-radius);
}
.fos-preview__label {
    font-size: 12px; font-weight: 700; text-transform: uppercase;
    letter-spacing: .08em; color: var(--fos-text-muted);
    margin-bottom: var(--fos-s3);
}
/* overflow-wrap:anywhere, not break-word. A public link is ninety characters
   with no space in it, and only `anywhere` lets the panel size itself below its
   longest unbreakable word — with break-word the URL sets the width and walks
   out through the side of the card. */
.fos-preview__body { white-space: pre-wrap; overflow-wrap: anywhere; }

/* --- Tables --------------------------------------------------------------- */

/* Wrap every table in one of these. A table of real data doesn't fit a phone,
   and the alternatives are worse than scrolling: squeezing columns makes an
   address unreadable, and hiding them hides the thing someone came for. So the
   table keeps its shape and scrolls sideways inside its own card, while the
   page itself never does. */
.fos-table-wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}
.fos-table-wrap .fos-table { min-width: 560px; }

/* Closing a task: the row stays a row, and this opens underneath it at full
   width. The first version lived in the right-hand column beside the title,
   which put a note field and a file picker in the right half of the card with
   the task they belong to in the left. */
.fos-close { margin-top: var(--fos-s2); }
/* Carries .fos-btn for everything else — this only takes the marker off and
   makes a <summary> sit like a button. */
.fos-close__open { display: inline-block; list-style: none; }
.fos-close__open::-webkit-details-marker { display: none; }
.fos-close[open] > .fos-close__open { margin-bottom: 0; }
.fos-close__panel {
    margin-top: var(--fos-s3);
    padding: var(--fos-s4);
    background: var(--fos-surface-alt);
    border-radius: 9px;
}
.fos-close__panel .fos-input { width: 100%; }
.fos-close__file { margin-top: var(--fos-s3); }
.fos-close__file input[type=file] { display: block; margin: 4px 0; font-size: 12.5px; }
.fos-close__must {
    color: var(--fos-warn); font-size: 11px; text-transform: uppercase;
    letter-spacing: .05em;
}
/* The optional one stays folded away. "Choose Files / No file chosen" on every
   task is noise on the ninety per cent that need nothing. */
.fos-close__maybe { margin-top: var(--fos-s3); font-size: 12.5px; }
.fos-close__maybe summary { cursor: pointer; color: var(--fos-text-muted); }
.fos-close__maybe input[type=file] { display: block; margin-top: 6px; font-size: 12.5px; }
.fos-close__buttons {
    display: flex; gap: var(--fos-s2); align-items: center;
    flex-wrap: wrap; margin-top: var(--fos-s4);
}
.fos-close__buttons .fos-faint { font-size: 11.5px; }

/* On a phone, a table marked .fos-stack stops being a table.
 *
 * The min-width above is what holds a table open for its sideways scroll, and
 * that is the right answer for a grid of figures. It is the wrong answer for a
 * list — a three-column row is 560px in a 277px card, so half of every row is
 * off the side of the screen with nothing saying so. Add .fos-stack to the
 * wrap and each row becomes a block. */
@media (max-width: 720px) {
    .fos-stack .fos-table { min-width: 0; }
    .fos-stack table, .fos-stack tbody, .fos-stack tr, .fos-stack td {
        display: block; width: auto;
    }
    .fos-stack thead { display: none; }
    .fos-stack .fos-table tr {
        padding: var(--fos-s3) 0;
        border-bottom: 1px solid var(--fos-rule);
    }
    .fos-stack .fos-table tr:last-child { border-bottom: 0; }
    /* `.fos-table td` sets a bottom border and beats a bare `td` selector, so
       every stacked cell drew a rule and one row read as three. */
    .fos-stack .fos-table td {
        padding: 1px 0; border: 0; text-align: left !important;
        vertical-align: baseline;
    }
    /* The label the hidden header used to carry, on the cells that need one —
       a bare date under a name reads as nothing. */
    .fos-stack .fos-table td[data-label]::before {
        content: attr(data-label) " ";
        font-size: 11px; text-transform: uppercase;
        letter-spacing: .05em; opacity: .55;
    }
    .fos-diff__peek { white-space: normal; max-width: none; overflow: visible; }
}

/* The same scroll, sized to the content instead of to a guess. Some tables are
   narrow by nature — two columns of short values, a reference list — and
   holding those open at 560px invents a sideways scroll for content that fits.

   `width: max-content` rather than `min-width: 0`, which is the obvious version
   and the wrong one: with a plain width:100% the table then collapses to its
   *min*-content, and every cell wraps as hard as it possibly can — a 35-word
   note in a 78px column. max-content asks for the width the table would like,
   `min-width: 100%` stops a short one sitting in half a card, and the wrapper
   scrolls only when the two disagree with the phone. Not for tables carrying
   paragraphs: max-content on a 500-character cell is a 4,000px line. */
.fos-table-wrap--fit .fos-table { width: max-content; min-width: 100%; }

.fos-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
/* Long unbroken strings — an email address, a URL — break rather than push the
   table wider than its card. Without this the table's min-content width is the
   length of the longest word, so one long address ran off the side of a phone
   and took every other value with it.

   `anywhere` rather than `break-word`: only the former is counted when the
   table works out its intrinsic width, which is the number doing the damage.

   Value cells only, and that distinction is the whole rule. Applied to every
   cell it also licenses the *label* column to break, and the browser took it:
   "Received" became "Receive/d" and "Customer" became "Custom/er" down the left
   of the card. Labels are short words that should never break; leaving them
   alone keeps their column at its natural width and lets the value column
   absorb the squeeze, which is the behaviour wanted in the first place. */
.fos-table td.num { white-space: nowrap; }

/* …except in a label/value card, which is where that rule came from. Those
   hold sentences and street names in the value column, not figures, and they
   are the ones that needed to absorb a squeeze. A numeric column must not:
   "$174.10" broken over two lines is "$174." above "10", which is not a
   number anybody can read, and "4.50" became "4.5" above "0". The table sits
   in `fos-table-wrap`, so what gives instead is a sideways scroll. */
.fos-facts td.num,
.fos-table--facts td.num {
    white-space: normal;
    overflow-wrap: anywhere;
}

/* In a flush card the header carries the card's own padding while the table
   carries a cell's, so the title sat four pixels right of the first value under
   it (eight on desktop) — close enough to look like a mistake and far enough to
   see. The outer columns take the header's padding instead, so the first value
   starts under the title and the last ends under the badge. Inner columns keep
   the tighter cell padding, which is what makes a table readable. */
.fos-card--flush .fos-table > * > tr > :first-child { padding-left: var(--fos-s5); }
.fos-card--flush .fos-table > * > tr > :last-child { padding-right: var(--fos-s5); }
.fos-table th {
    text-align: left;
    font-size: 11.5px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--fos-text-muted);
    padding: var(--fos-s2) var(--fos-s3);
    border-bottom: 1px solid var(--fos-rule-strong);
    white-space: nowrap;
}
/* The legacy app makes every <th> click-to-sort with no affordance at all.
   Staff rely on it, so keep the behavior — but say so. */
.fos-table th[data-sortable] { cursor: pointer; user-select: none; }
/* A column heading carrying an explanation says so, rather than hiding it
   behind a hover somebody has to guess is there. */
/* A card that opens says so, on the side the eye is already on. Safari keeps
   its own triangle unless it is turned off explicitly, so both are handled and
   only one marker is ever drawn. */
/* The whole closed card is the target, not the text in it.
 *
 * A <summary> is only as big as its contents, so the card's own padding was a
 * dead border around it — clicking an inch from a technician's name did
 * nothing, on a card whose entire purpose is to open. The summary is pulled out
 * to the card's edges and given that padding back, so the hit area is the card.
 *
 * The negative margin has to match `.fos-card`'s padding, including the
 * narrower one on mobile below. */
details.fos-card > summary {
    position: relative;
    margin: calc(-1 * var(--fos-s5));
    padding: var(--fos-s5);
    padding-right: calc(var(--fos-s5) + 18px);
    border-radius: var(--fos-radius-lg);
}
details.fos-card[open] > summary {
    margin-bottom: 0;
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}
/* A --flush card has no padding, so there is nothing for that bleed to cancel:
   the summary hung 20px off each side and the card's own `overflow: hidden`
   clipped it — visible as a sliver of header cut off at both ends. The bleed
   exists to reach the edge of a padded card; on a flush one it is already
   there. */
details.fos-card--flush > summary {
    margin: 0;
    padding: var(--fos-s4);
    padding-right: calc(var(--fos-s4) + 18px);
}

details.fos-card > summary:hover { background: var(--fos-cream); }
details.fos-card > summary::-webkit-details-marker { display: none; }
details.fos-card > summary::after {
    content: '\203A';
    position: absolute; right: var(--fos-s5); top: 50%;
    transform: translateY(-50%) rotate(90deg);
    transition: transform .12s ease;
    color: var(--fos-text-muted); font-size: 18px; line-height: 1;
}
details.fos-card[open] > summary::after { transform: translateY(-50%) rotate(-90deg); }
details.fos-card > summary:hover::after { color: var(--fos-text); }

.fos-table th[title] { cursor: help; }
/* Two rules, not one comma-separated pair. A selector list containing an
   unsupported `:has()` is thrown away whole by older parsers, which would take
   the sortable-heading underline with it — so the common case stands alone. */
.fos-table th[title] {
    text-decoration: underline dotted var(--fos-rule-strong);
    text-underline-offset: 3px;
}
/* The sorting link inherits it rather than drawing a second line under it. */
.fos-table th[title] > a { text-decoration: none; }
.fos-table th[data-sortable]::after {
    content: '⇅';
    margin-left: 5px;
    opacity: 0.35;
    font-size: 10px;
}
.fos-table th[data-sortable]:hover::after { opacity: 0.8; }
.fos-table th[data-sort-dir='asc']::after  { content: '↑'; opacity: 0.9; }
.fos-table th[data-sort-dir='desc']::after { content: '↓'; opacity: 0.9; }

.fos-table td {
    padding: var(--fos-s3);
    border-bottom: 1px solid var(--fos-rule);
    vertical-align: middle;
}
.fos-table tbody tr:last-child td { border-bottom: 0; }
.fos-table tbody tr:hover { background: var(--fos-surface-alt); }
.fos-table .num { text-align: right; font-variant-numeric: tabular-nums; }
/* A figure, and never broken across lines wherever it appears. */
.fos-money {
    font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap;
}

.fos-empty {
    text-align: center;
    color: var(--fos-text-faint);
    padding: var(--fos-s8) var(--fos-s4);
    font-style: italic;
}
.fos-muted { color: var(--fos-text-muted); }
.fos-faint { color: var(--fos-text-faint); }

/* --- Forms ---------------------------------------------------------------- */

.fos-field { margin-bottom: var(--fos-s4); }
.fos-label {
    display: block;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--fos-text);
    margin-bottom: 5px;
}
.fos-input,
input[type='text'], input[type='email'], input[type='password'],
input[type='number'], input[type='date'], input[type='datetime-local'],
input[type='time'], input[type='month'], input[type='url'],
input[type='tel'], input[type='search'], select, textarea {
    width: 100%;
    padding: 9px 12px;
    font-family: var(--fos-font);
    font-size: 14px;
    color: var(--fos-text);
    background: var(--fos-surface);
    border: 1.5px solid #D8DDE2;
    border-radius: var(--fos-radius-sm);
    transition: border-color var(--fos-ease), box-shadow var(--fos-ease);
}
input:focus, select:focus, textarea:focus {
    outline: none;
    border-color: var(--fos-teal);
    box-shadow: 0 0 0 3px rgba(62, 193, 205, 0.18);
}
input[type='checkbox'], input[type='radio'] { width: auto; }

/* Textareas grow downwards only. The browser default is `resize: both`, which
   lets the drag handle pull a box wider than the card it sits in — the text
   then runs over the card's edge and under whatever is beside it. Height is
   the dimension anyone actually wants: a long estimate summary needs more
   lines, never a wider column. max-width is the belt to that braces, for any
   textarea given an explicit width elsewhere. */
textarea {
    resize: vertical;
    max-width: 100%;
}

/* Date and time pickers, which mobile Safari styles to its own taste.
   Two things it does that nothing else on a form does: it centres the value,
   so a date sits in the middle of a full-width box while every label and every
   other value is left-aligned; and it sizes the control to its content, so a
   time field shrinks to a small pill beside the full-width selects above and
   below it.

   Left-aligned and full-height here, so a row of fields reads as a row. The
   picker indicator is deliberately left alone — appearance:none would take the
   calendar and clock buttons with it, and those are how you actually use these
   on a phone. */
input[type='date'], input[type='time'],
input[type='datetime-local'], input[type='month'] {
    text-align: left;
}
.fos-help { font-size: 12px; color: var(--fos-text-muted); margin-top: 4px; }

/* What the selected job type means, under the picker. A rule down the left
   rather than plain help text: it changes as the CSR clicks through the types,
   and a line that rewrites itself needs to look like it belongs to the control
   above it, or it reads as the page shifting under them mid-call. */
/* "The next few days are nearly empty — offer the earliest time." An
   instruction to the CSR, not a description of the times, so it reads as a
   notice rather than more help text: warm wash, rule down the left, and dark
   enough to be read rather than skimmed past. */
/* The week's floor report on the dispatch board. Quieter than the conflict
   badge — nobody is double-booked and nothing is broken; somebody is simply
   going to be paid short unless the day gets rearranged. */
.board__floor { margin-bottom: var(--fos-s4); }
.board__floor__team {
    display: flex;
    flex-wrap: wrap;
    gap: var(--fos-s2) var(--fos-s3);
    align-items: baseline;
    padding: var(--fos-s2) 0;
    border-top: 1px solid var(--fos-rule);
}
.board__floor__team:first-of-type { border-top: 0; }
.board__floor__people { display: flex; flex-wrap: wrap; gap: var(--fos-s2); }
.board__floor__person {
    border-radius: var(--fos-radius-sm);
    padding: 2px var(--fos-s2);
    font-size: 12.5px;
    white-space: nowrap;
}
/* Red is short of the floor, green is clear of it — the two states this panel
   exists to tell apart, so the colour carries it and nobody has to compare
   each number against the floor in the heading. Tinted backgrounds rather than
   coloured text: these sit in a row and the fill is what reads at a glance.
   Colour is never the only signal — the hours are on the chip and the shortfall
   is in the hover text. */
.board__floor__person.is-short {
    background: var(--fos-danger-bg, rgba(198, 40, 40, 0.10));
    color: var(--fos-danger, #b3261e);
}
.board__floor__person.is-over {
    background: var(--fos-ok-bg, rgba(15, 123, 92, 0.10));
    color: var(--fos-ok, #0f7b5c);
}
.board__floor__hours {
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    margin-left: 4px;
}

/* The leaderboard. One table doing both jobs it used to take two for: every
   CSR's week, and the desk's own line at the foot to read them against. The
   viewer's row is marked so they can find it without reading. */
.fos-leaderboard { margin-bottom: var(--fos-s4); }
.fos-leaderboard .fos-table { margin-bottom: var(--fos-s2); }
.fos-leaderboard .fos-table th { font-size: 11.5px; }
/* Green, not the brand red. Red in a table row reads as an alert — something
   is wrong with this one — and the row it marks is just the reader's own. It
   sat on the top row of a two-person desk, so it looked like a verdict on the
   leader rather than a "you are here". */
.fos-leaderboard tr.is-me { background: var(--fos-ok-bg); }
/* The two desk lines, ruled off from the people above them. Only the first
   carries the rule — the two belong together and a line between them would
   read as three separate blocks. */
.fos-leaderboard tfoot tr:first-child td { border-top: 2px solid var(--fos-rule); }
.fos-leaderboard tfoot tr.is-average td { background: var(--fos-surface-alt); }

/* The "why the shortcuts don't pay" note. Folded away — it's read once and
   remembered, and an always-open essay on the inbox is noise for the other 51
   weeks. The summary line carries the deterrence on its own. */
.fos-score-how > summary {
    cursor: pointer;
    font-size: 12.5px;
    color: var(--fos-text-muted);
    padding: var(--fos-s2) 0;
}
.fos-score-how > summary:hover { color: var(--fos-text); }
.fos-score-how p { font-size: 12.5px; color: var(--fos-text-muted); }
.fos-score-how .fos-table { font-size: 12.5px; margin: var(--fos-s3) 0; }
.fos-score-how .fos-table td { vertical-align: top; }
/* The row that isn't a shortcut. Its own class rather than borrowing the
   leaderboard's "you are here" — one means the reader, the other means the
   right answer, and a class doing both is how the red ended up reading as a
   judgement on whoever was top. */
.fos-score-how tr.is-good { background: var(--fos-ok-bg); }

/* The customer-text settings: each notice a bordered block — switch, textarea,
   live example — so the three read as three separate things to turn on and word.
   
   Named .fos-notice, not .fos-msg. It was .fos-msg, which the SMS thread had
   already claimed for a chat bubble — and being the later rule in this file it
   put a bordered, padded box around every bubble in every conversation. Two
   unrelated things under one class, and the collision was invisible until you
   looked at a thread. */
.fos-notice { border: 1px solid var(--fos-rule); border-radius: var(--fos-radius);
    padding: var(--fos-s4); margin-bottom: var(--fos-s4); }
.fos-notice .fos-check { display:flex; gap:8px; align-items:center; margin-bottom:var(--fos-s3); font-size:13.5px; }
.fos-notice__preview { margin-top: var(--fos-s3); padding: var(--fos-s3);
    background: var(--fos-surface-alt); border-radius: var(--fos-radius-sm); font-size: 14px; }

/* The on-call opt-in under the day list, and the premium day it unlocks. Gold,
   the brand's accent for "this costs / this is special", so an on-call day reads
   as different-and-paid rather than just another option. */
.fos-oncall {
    display: flex; align-items: center; gap: var(--fos-s2);
    margin-top: var(--fos-s3); padding-top: var(--fos-s3);
    border-top: 1px solid var(--fos-rule);
    font-size: 13px; cursor: pointer;
}
.fos-oncall #on-call-fee { color: var(--fos-text-muted); }
.fos-slot.is-on-call { border-color: #B8860B; }
.fos-slot.is-on-call .fos-slot__who { color: #B8860B; font-weight: 600; }

.fos-board-note {
    background: var(--fos-warn-bg);
    border-left: 3px solid var(--fos-warn);
    border-radius: var(--fos-radius-sm);
    color: var(--fos-text);
    font-size: 12.5px;
    padding: var(--fos-s2) var(--fos-s3);
    margin-bottom: var(--fos-s3);
}

.fos-type-note {
    border-left: 2px solid var(--fos-rule);
    padding-left: var(--fos-s2);
    margin-top: var(--fos-s2);
}

/* A group of fields that only applies in some cases — indented and tinted so
   it reads as "these belong to the thing above", and toggled with [hidden]. */
.fos-subgroup {
    background: var(--fos-cream-light);
    border: 1px solid var(--fos-rule);
    border-left: 3px solid var(--fos-teal);
    border-radius: var(--fos-radius);
    padding: var(--fos-s4);
    margin: 0 0 var(--fos-s4) 26px;
}
.fos-subgroup[hidden] { display: none; }
.fos-subgroup__title {
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--fos-text-muted);
    margin-bottom: var(--fos-s3);
}
.fos-error {
    font-size: 12.5px;
    color: var(--fos-danger);
    margin-top: 5px;
    font-weight: 600;
}
/* The neutral one, and it needed a look of its own.
   `.fos-alert` carried padding and a radius and no background, so a bare one —
   which is seventeen of them across the app — rendered as unstyled text
   floating on the page. Not obviously broken, just wrong: the words sat where
   a panel should be and read as a stray paragraph under whatever was above
   them. The modifiers below are for alerts that mean something; this is for
   the ones that are simply worth reading. */
.fos-alert {
    padding: var(--fos-s3) var(--fos-s4);
    border-radius: var(--fos-radius-sm);
    font-size: 13.5px;
    margin-bottom: var(--fos-s4);
    background: var(--fos-surface-alt);
    border: 1px solid var(--fos-rule);
    color: var(--fos-text);
}
/* The call bar: the same panel, with the button on the end of it.
   The button sits inside a form of its own, which is a block — so without
   this the "End call" lands on its own line under the sentence and the bar
   is twice the height it needs to be. */
.fos-call-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--fos-s4);
    flex-wrap: wrap;
}
.fos-call-bar form { margin: 0; }

/* The coloured ones say what they are with colour, so they do not want a rule
   as well — a border on a tinted panel reads as a second, weaker box. */
.fos-alert--error, .fos-alert--ok, .fos-alert--warn { border-color: transparent; }
.fos-alert--error { background: var(--fos-danger-bg); color: var(--fos-danger); }
.fos-alert--ok { background: var(--fos-ok-bg); color: var(--fos-ok); }
/* Something to know about, not something that went wrong — a refused email
   attachment, not a failed send. Reaching for --error here would put the two on
   the same footing and teach people to ignore both. */
.fos-alert--warn { background: var(--fos-warn-bg); color: var(--fos-warn); }

/* --- Two-factor ----------------------------------------------------------- */

/* Big, monospaced, well-spaced — a six-digit code is read off a phone and
   typed under mild time pressure. */
.fos-otp-input {
    font-family: var(--fos-font-mono);
    font-size: 26px;
    letter-spacing: 0.28em;
    text-align: center;
    padding: 12px;
}
.fos-qr {
    display: flex;
    justify-content: center;
    padding: var(--fos-s4);
    background: #fff;
    border: 1px solid var(--fos-rule);
    border-radius: var(--fos-radius);
}
.fos-qr svg { width: 220px; height: 220px; display: block; }
.fos-secret {
    display: block;
    font-family: var(--fos-font-mono);
    font-size: 14px;
    letter-spacing: 0.1em;
    background: var(--fos-cream);
    border: 1px solid var(--fos-rule);
    border-radius: var(--fos-radius-sm);
    padding: var(--fos-s3);
    word-break: break-all;
    user-select: all;
}
.fos-codes {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: var(--fos-s2);
}
.fos-codes code {
    font-family: var(--fos-font-mono);
    font-size: 15px;
    letter-spacing: 0.08em;
    text-align: center;
    background: var(--fos-cream);
    border: 1px solid var(--fos-rule);
    border-radius: var(--fos-radius-sm);
    padding: 9px;
    user-select: all;
}
@media print {
    .fos-topbar, .fos-page-head, .fos-btn, .fos-alert, .fos-help { display: none !important; }
    .fos-card { border: 0; box-shadow: none; }

    /* Four templates marked their internal blocks `fos-no-print` and the class
       was never written, so it hid nothing: the invoice view's Print button put
       gross profit and margin on a page somebody hands to the customer. The
       comment above that wrapper said it "guarantees" the costing stays off the
       printout. It guaranteed nothing until this rule existed. */
    .fos-no-print { display: none !important; }

    /* With the internal column gone, the document should have the paper to
       itself rather than sitting in a 1fr of a two-column grid. */
    .fos-split { display: block !important; }

    /* The mirror of fos-no-print: things that exist only on paper. A signed
       quote's signature lives here — on screen the Outcome card says it, and
       that card is an office control panel that must not print. */
    .fos-print-only { display: block !important; }
    .fos-signature { margin-top: 28px; }
    .fos-signature__label {
        font-size: 11px; letter-spacing: .09em; text-transform: uppercase;
        font-weight: 700; margin-bottom: 6px;
    }
    .fos-signature__name {
        font-family: "Segoe Script", "Snell Roundhand", cursive;
        font-size: 22px; display: inline-block; min-width: 260px;
        border-bottom: 1px solid #000; padding: 0 8px 3px 0; margin-bottom: 10px;
    }
    .fos-signature__detail {
        display: grid; grid-template-columns: auto 1fr; gap: 4px 14px;
        margin: 0; font-size: 13px;
    }
    .fos-signature__detail dd { margin: 0; }
}

/* Hidden everywhere but the printout. Declared outside the print block so it
   is the default, with the rule above turning it on. */
.fos-print-only { display: none; }

/* --- Login ---------------------------------------------------------------- */

.fos-login {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--fos-s6);
    background: var(--fos-bg);
}
.fos-login__box { width: 100%; max-width: 380px; }
.fos-login__brand { text-align: center; margin-bottom: var(--fos-s6); }
.fos-login__brand .fos-wordmark { font-size: 44px; }
/* Sign-in sets neither tagline nor foot — the logo stands alone there — so
   collapse the empty <p>s rather than letting their margins add dead space. */
.fos-login__tagline:empty,
.fos-login__foot:empty { display: none; }
.fos-login__tagline {
    margin: var(--fos-s3) 0 0;
    color: var(--fos-text-muted);
    font-size: 13px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    font-weight: 600;
}
.fos-login__card {
    background: var(--fos-surface);
    border: 1px solid var(--fos-rule);
    border-radius: var(--fos-radius-lg);
    box-shadow: var(--fos-shadow);
    padding: var(--fos-s6);
}
.fos-login__foot {
    text-align: center;
    margin-top: var(--fos-s5);
    font-size: 12px;
    color: var(--fos-text-faint);
}

/* --- Small screens -------------------------------------------------------- *
 *
 * The office runs this on desktops, but a dispatcher checking the board from a
 * van or a phone at home is real, and until now that meant a topbar whose nav
 * ran off the right edge — Settings and Sign out simply unreachable.
 *
 * The breakpoint is 1200px, not a phone width. It was 860, which was chosen
 * for the tablet case and left a band nobody had measured: the bar wants a
 * logo, a search box and thirteen nav items, which came to 1280px, so on every
 * laptop between 861 and about 1340 the nav ran off the right edge and dragged
 * the whole page's horizontal scroll with it. Not a wide-page problem — the
 * dashboard did it too.
 *
 * Tighter nav items above the breakpoint bring the bar down to 1154, so a
 * 1280-wide laptop keeps the full nav and anything narrower gets the drawer,
 * which is the control that was built for exactly this.
 */

.fos-topbar__toggle { display: none; }

/* display:contents makes the wrapper vanish from layout entirely: the search box
   and nav become flex children of __inner, so the desktop topbar is byte for
   byte what it was before the mobile drawer needed something to wrap. */
.fos-topbar__menu { display: contents; }

/* Thirteen items, and the bar has to hold a logo and a search box as well.
   At the old padding the nav alone wanted 997px and the bar 1280 — so on any
   laptop between the burger breakpoint and about 1340 it simply ran off the
   right-hand edge and took the whole page's horizontal scroll with it. Every
   page, not just the wide ones.

   Tighter items buy 126px of that back; the breakpoint below buys the rest. */
@media (min-width: 1201px) {
    .fos-nav a,
    .fos-nav__logout button { padding: 7px var(--fos-s2); font-size: 13px; }
}

@media (max-width: 1200px) {
    .fos-topbar__inner { padding: 0 var(--fos-s4); gap: var(--fos-s3); height: 52px; }

    /* Push the button to the right edge, whatever else is in the bar. */
    .fos-topbar__toggle {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        margin-left: auto;
        width: 44px;                 /* a real touch target, not a 24px glyph */
        height: 44px;
        background: none;
        border: 0;
        border-radius: var(--fos-radius-sm);
        cursor: pointer;
    }
    .fos-topbar__toggle:hover { background: rgba(255, 255, 255, 0.10); }

    /* Three lines that become an X when open — drawn in CSS so the topbar still
       needs no icon system, same reasoning as the inlined search magnifier. */
    .fos-burger, .fos-burger::before, .fos-burger::after {
        content: '';
        display: block;
        position: absolute;
        width: 18px;
        height: 2px;
        background: #fff;
        border-radius: 2px;
        transition: transform var(--fos-ease), opacity var(--fos-ease);
    }
    .fos-burger { position: relative; }
    .fos-burger::before { transform: translateY(-6px); }
    .fos-burger::after  { transform: translateY(6px); }
    [aria-expanded='true'] .fos-burger { background: transparent; }
    [aria-expanded='true'] .fos-burger::before { transform: rotate(45deg); }
    [aria-expanded='true'] .fos-burger::after  { transform: rotate(-45deg); }

    /* The drawer. Sits under the bar rather than over the page: no backdrop, no
       focus trap, no scroll lock — three things that go wrong in exactly the
       situation where someone is one-handed on a roof. */
    .fos-topbar__menu {
        display: none;
        position: absolute;
        top: 100%;
        left: 0;
        right: 0;
        background: var(--fos-topbar);
        border-top: 1px solid rgba(255, 255, 255, 0.10);
        padding: var(--fos-s4);
        box-shadow: 0 12px 24px rgba(0, 0, 0, 0.18);
    }
    .fos-topbar__menu.is-open { display: block; }

    .fos-search { max-width: none; margin: 0 0 var(--fos-s3); flex: none; }

    /* Stacked rows, each a full-width tap target. */
    .fos-nav { flex-direction: column; align-items: stretch; gap: 2px; }
    .fos-nav a, .fos-nav__logout button {
        display: block;
        width: 100%;
        text-align: left;
        padding: 11px var(--fos-s3);
        font-size: 15px;
        border-radius: var(--fos-radius-sm);
    }
    .fos-nav__sep {
        width: auto;
        height: 1px;
        margin: var(--fos-s2) 0;
        background: rgba(255, 255, 255, 0.14);
    }
    .fos-nav__logout { width: 100%; }

    /* The panel is positioned against __inner, so it needs a containing block. */
    .fos-topbar__inner { position: relative; }
}

@media (max-width: 700px) {
    .fos-container { padding: var(--fos-s4) var(--fos-s4) var(--fos-s8); }
    .fos-page-head__title { font-size: 24px; }

    /* iOS zooms the whole page in when you focus an input under 16px, then
       leaves you scrolled sideways. Costs nothing on desktop; this rule only
       applies here. */
    .fos-input,
    input[type='text'], input[type='email'], input[type='password'],
    input[type='number'], input[type='date'], input[type='datetime-local'],
    input[type='time'], input[type='month'], input[type='url'],
    input[type='tel'], input[type='search'], select, textarea {
        font-size: 16px;
    }

    /* Thumbs, not mice. */
    .fos-btn { padding: 11px 16px; }
    .fos-btn--lg { padding: 13px 18px; }
    /* Bigger tap targets on the segmented control (board filters, inbox tabs). */
    .fos-segmented a { padding: 9px 14px; }

    /* A page-head action row reads better as full-width buttons than as three
       cramped ones sharing a line with a badge. */
    .fos-page-head { align-items: stretch; }

    .fos-card { padding: var(--fos-s4); }
    /* In step with the card padding above, so every title on the page starts
       at the same x however its card is built. */
    .fos-card__head { padding: var(--fos-s4) var(--fos-s4) 0; }
    /* Same step down, so the table stays under its own header. */
    .fos-card--flush .fos-table > * > tr > :first-child { padding-left: var(--fos-s4); }
    .fos-card--flush .fos-table > * > tr > :last-child { padding-right: var(--fos-s4); }
    .fos-subgroup { margin-left: 0; }
    .fos-qr svg { width: 100%; height: auto; max-width: 220px; }
}

/* --- Attachments ---------------------------------------------------------- */

/* `auto-fit`, not `auto-fill`. auto-fill keeps the empty columns it could not
   fill, so a card of three photographs on a wide screen laid three small tiles
   against the left edge and left two thirds of the card blank; a full page of
   ten laid seven across and three under them, ragged. auto-fit drops the empty
   tracks and the tiles take the room back.

   The cap is what keeps that from going the other way. Without one, a single
   file on a wide screen becomes a metre-wide square of somebody's capacitor —
   so a tile grows to 200px and no further, and what is left over is spread
   between them rather than piled at the right-hand end. Ten of them then fill
   the row edge to edge at any width, and three sit evenly across it instead of
   huddling. */
.fos-attach-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 200px));
    justify-content: space-between;
    gap: var(--fos-s3);
}
.fos-attach { display: flex; flex-direction: column; gap: 4px; }
.fos-attach__thumb {
    display: block;
    aspect-ratio: 1;
    border-radius: var(--fos-radius-sm);
    border: 1px solid var(--fos-rule);
    overflow: hidden;
    background: var(--fos-surface-alt);
}
.fos-attach__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.fos-attach__thumb--pdf {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--fos-red);
    font-weight: 800;
    font-size: 15px;
    letter-spacing: 0.06em;
}
.fos-attach__thumb video { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A still frame with a play mark over it, rather than a control bar shrunk to
   120px. The tile is a link — playing happens on the page it opens. */
.fos-attach__thumb--video { position: relative; }
.fos-attach__thumb--video::after {
    content: "\25B6";
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.18);
    color: #fff;
    font-size: 22px;
    text-shadow: 0 1px 6px rgba(0, 0, 0, 0.6);
    pointer-events: none;
}
.fos-attach__meta { display: flex; align-items: center; gap: 4px; min-width: 0; }
.fos-attach__name {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 11.5px;
    color: var(--fos-text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.fos-attach__del {
    flex: 0 0 auto;
    border: 0;
    background: none;
    color: var(--fos-text-faint);
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
    padding: 0 2px;
    border-radius: var(--fos-radius-sm);
}
.fos-attach__del:hover { color: var(--fos-danger); background: var(--fos-danger-bg); }
.fos-attach__thumb[data-photo] { cursor: zoom-in; }

/* --- Photo lightbox ------------------------------------------------------- */
/* A native <dialog>, so the browser handles Escape, focus and the top layer.
   Everything below only has to lay out the photo and its controls. */

.fos-lightbox {
    border: 0;
    padding: 0;
    background: none;
    max-width: 100vw;
    max-height: 100vh;
    width: 100vw;
    height: 100vh;
    color: #fff;
    overflow: hidden;
}
.fos-lightbox::backdrop { background: rgba(18, 24, 30, 0.93); }

/* A sheet: a short list on top of the page rather than instead of it.
   Sized to its content up to a cap, so three contacts do not get the same box
   as thirty, and the list scrolls inside while the head and foot stay put —
   the foot carries the way through to the record, which is the thing somebody
   wants after reading it. */
.fos-sheet {
    border: 1px solid var(--fos-rule-strong);
    border-radius: var(--fos-radius-lg);
    padding: 0;
    background: var(--fos-surface);
    color: var(--fos-text);
    width: min(440px, calc(100vw - var(--fos-s6)));
    max-height: min(70vh, 640px);
    box-shadow: 0 24px 60px -24px rgba(45, 58, 71, 0.5);
    overflow: hidden;
}
.fos-sheet::backdrop { background: rgba(18, 24, 30, 0.42); }
.fos-sheet__head {
    display: flex; align-items: flex-start; gap: var(--fos-s3);
    padding: var(--fos-s4);
    border-bottom: 1px solid var(--fos-rule);
}
.fos-sheet__title {
    font-size: 11.5px; font-weight: 700; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--fos-text-muted);
}
.fos-sheet__close {
    margin-left: auto; border: 0; background: none; cursor: pointer;
    color: var(--fos-text-faint); font-size: 20px; line-height: 1;
    padding: 0 2px; border-radius: var(--fos-radius-sm);
}
.fos-sheet__close:hover { color: var(--fos-danger); background: var(--fos-danger-bg); }
.fos-sheet__body { overflow-y: auto; max-height: calc(70vh - 128px); }
.fos-sheet__row {
    padding: var(--fos-s3) var(--fos-s4);
    border-bottom: 1px solid var(--fos-rule);
    font-size: 13.5px;
}
.fos-sheet__row:last-child { border-bottom: 0; }
/* A button that reads as a link. For a control that opens something on this
   page rather than going anywhere: an anchor would offer a new tab on a middle
   click and a URL on hover, and neither is a promise it can keep. */
.fos-linkish {
    border: 0; background: none; padding: 0; cursor: pointer;
    font-family: inherit; font-size: inherit;
    color: var(--fos-red-dark); text-decoration: underline;
}
.fos-linkish:hover { color: var(--fos-red); }

.fos-sheet__foot {
    padding: var(--fos-s3) var(--fos-s4);
    border-top: 1px solid var(--fos-rule);
    background: var(--fos-surface-alt);
    font-size: 13px;
}

/* [open] and not the bare element: a <dialog> is display:none until it opens,
   and setting display unconditionally would show it on every page load. */
.fos-lightbox[open] {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-rows: minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--fos-s3);
    padding: var(--fos-s4);
}
.fos-lightbox__img {
    grid-column: 2;
    grid-row: 1;
    max-width: 100%;
    max-height: 100%;
    margin: 0 auto;
    object-fit: contain;      /* whole photo, never cropped — the point of opening it */
    border-radius: var(--fos-radius-sm);
}
.fos-lightbox__gone {
    grid-column: 2;
    grid-row: 1;
    text-align: center;
    margin: 0;
    font-size: 14px;
    opacity: 0.85;
}
.fos-lightbox__nav {
    grid-row: 1;
    width: 46px;
    height: 46px;
    border: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.12);
    color: #fff;
    font-size: 30px;
    line-height: 1;
    cursor: pointer;
    transition: background 0.12s ease;
}
.fos-lightbox__nav:hover { background: rgba(255, 255, 255, 0.24); }
.fos-lightbox__nav[hidden] { display: none; }
.fos-lightbox__close {
    position: absolute;
    top: var(--fos-s3);
    right: var(--fos-s3);
    width: 40px;
    height: 40px;
    border: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.12);
    color: #fff;
    font-size: 24px;
    line-height: 1;
    cursor: pointer;
}
.fos-lightbox__close:hover { background: rgba(255, 255, 255, 0.24); }
.fos-lightbox__bar {
    grid-column: 1 / -1;
    grid-row: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--fos-s4);
    flex-wrap: wrap;
    font-size: 12.5px;
    opacity: 0.8;
}
.fos-lightbox__bar a { color: #fff; text-decoration: underline; }

@media (max-width: 640px) {
    /* Thumbs need the width more than the arrows do; keep them, shrink them. */
    .fos-lightbox[open] { gap: var(--fos-s2); padding: var(--fos-s2); }
    .fos-lightbox__nav { width: 38px; height: 38px; font-size: 26px; }
}

/* Routing preference. The derived label is the thing being read; the number
   behind it never appears, because a bare 65 tells nobody anything. */
.fos-policy { text-align: center; }
.fos-policy__label { font-family: var(--fos-font-display); font-size: 24px;
                     color: var(--fos-text); margin-bottom: var(--fos-s2); }
.fos-policy__ends { display: flex; justify-content: space-between;
                    font-size: 12.5px; color: var(--fos-text-muted);
                    margin-bottom: var(--fos-s1); }
.fos-policy input[type="range"] { width: 100%; accent-color: var(--fos-teal); }
/* The two weights, in the open. The phrase above says which way the setting
   leans; this says by how much — and "how much" is the part that decides
   whether a better closer can ever outrank a nearer technician, which the
   label alone gives no way to judge. Tabular figures so they don't jitter
   while the slider is being dragged. */
.fos-policy__split { font-size: 13px; color: var(--fos-text-muted);
                     font-variant-numeric: tabular-nums;
                     margin-bottom: var(--fos-s4); }
.fos-policy__split strong { color: var(--fos-text); font-weight: 600; }
.fos-policy__split em { font-style: normal; color: var(--fos-text-faint); }

/* ---------------------------------------------------------------- mobile --
   A filter panel on a phone.

   The panel is a flex row with a fixed basis on each field, which wraps
   sensibly on a laptop and falls apart on a 390px screen: the fields wrap to
   full width but Apply keeps its intrinsic width and stays on the last row,
   landing on top of the To date. A date input also carries a large intrinsic
   minimum width on iOS, so it will not shrink out of the way.

   One control per row, and Apply on its own, is the only layout that holds.  */
@media (max-width: 640px) {
    .fos-card form[method="get"] > div,
    .fos-card form[method="get"] > label {
        flex: 1 1 100% !important;
        min-width: 0;
    }
    .fos-card form[method="get"] > button {
        flex: 1 1 100%;
        width: 100%;
        margin-top: var(--fos-s2);
    }
    .fos-card form[method="get"] input,
    .fos-card form[method="get"] select {
        width: 100%;
        max-width: 100%;
        min-width: 0;
        box-sizing: border-box;
    }
    /* Safari only, and the reason this took three goes: a date input carries
       an intrinsic minimum width that width:100% does not override while the
       native control is in play, so on an iPhone the From and To fields ran
       wider than the selects above them and out of the card. Chromium sizes
       them correctly, which is why a desktop check at 375px kept passing.

       Dropping the native appearance lets the declared width win. The date
       picker still opens on tap — this changes the box, not the control. */
    .fos-card form[method="get"] input[type="date"] {
        -webkit-appearance: none;
        appearance: none;
        display: block;
        text-align: left;
    }

    /* A figure that breaks mid-digit reads as two numbers — "$3,314,452.6"
       above an "8" is worse than a smaller number, for the same reason a
       clipped one is. Never wrap; shrink to fit instead.

       Sized against the card, not the viewport. This was 5.9vw, which sounds
       like the same thing and isn't: the cards sit two to a row, so the box is
       barely a third of the viewport once padding and the gap come out. On a
       375px phone 5.9vw resolved to 22px in a 129px box, the 16px floor never
       got a chance to engage, and "$5,249,417.92" needed 160px — it ran out of
       its card and into the one beside it. cqi is a percentage of the card
       itself, so the number now shrinks when the card does.

       12cqi is the largest that still fits eight figures and cents: at 129px
       that's 15.5px of type for a string wanting 124px. Today's revenue is
       seven figures and clears by 17px, but this company grows ~19% a year and
       the eighth digit is a couple of years out — worth the 2px now rather
       than the same bug again then. Below ~360px the grid drops to one column
       on its own and the cap takes over. */
    .fos-stat { container-type: inline-size; }
    .fos-stat__value {
        white-space: nowrap;
        font-size: 16px;  /* pre-cqi browsers: fits seven figures, not eight */
        font-size: clamp(13px, 12cqi, 26px);
    }
}

/* A title inside a head does not draw its own rule.

   Both .fos-card__title and .fos-card__head carry a bottom border, from two
   different eras. On their own each is right: a bare title needs the rule, and
   a head with a button on the right needs it too. Nested, they draw twice —
   and because the head is a flex row the title shrinks to its text, so the
   inner one comes out as a stub underlining the words rather than the card.
   That is the odd short underline beneath "JOBS · 9" on search.

   The head owns the rule whenever there is one. A standalone title keeps its
   own, which is why this is scoped to the child rather than done by deleting
   the border above. */
.fos-card__head > .fos-card__title {
    padding-bottom: 0;
    margin-bottom: 0;
    border-bottom: 0;
}

/* Chart hover. CSS rather than script: the charts are server-rendered SVG and
   a readout is the one thing a static picture cannot do, but it does not need
   90KB of library — a :hover rule on a group is the whole mechanism.

   The group is drawn last in the markup so it paints over the lines without
   needing a z-index SVG does not have. */
.fos-spark__hover { opacity: 0; pointer-events: none; transition: opacity .08s ease-out; }
.fos-spark__pt:hover .fos-spark__hover,
.fos-spark__pt:focus-within .fos-spark__hover { opacity: 1; }


/* A proposal rung's label. The colour is inline because the office chooses it
   per rung; this only makes sure a word on a colour reads as a badge and not as
   a coloured word. */
.fos-badge--option {
    border: 0;
    font-weight: 600;
    letter-spacing: .02em;
    padding: 3px 9px;
    /* Pinned, so the pill cannot grow taller than the line it sits on and
       collide with the one below. It inherited the surrounding line-height and
       overlapped its neighbour wherever two were stacked. */
    line-height: 1.35;
}


/* A money input, with the currency in front of it rather than left to be
   inferred. The computed cells beside these have always printed "$12.34"; the
   boxes you type into printed "12.34", so the same column read two ways. */
.fos-money-input {
    position: relative;
    display: inline-block;
}
.fos-money-input::before {
    content: "$";
    position: absolute;
    left: 8px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--fos-text-muted);
    font-size: 13px;
    pointer-events: none;
}
.fos-money-input input {
    padding-left: 18px;
    text-align: right;
}

/* The same thing as a field in a form rather than a cell in a table.
   Full width, and the figure reads from the left where the label above it
   does — a lone amount right-aligned in a wide box sits nowhere near the
   thing naming it. */
.fos-money-input--field {
    display: block;
    width: 100%;
}
.fos-money-input--field::before {
    left: 12px;
    font-size: 15px;
}
.fos-money-input--field input {
    width: 100%;
    padding-left: 26px;
    text-align: left;
}

/* --- A membership, collapsed to its address ---------------------------------
 *
 * A plan is tied to one property, so a landlord with six houses on the
 * programme holds six memberships — and on the book today one account does,
 * one holds five and one holds four. Six full cards stacked is six walls of
 * near-identical tables with the one distinguishing fact, the address, buried
 * in the middle of each.
 *
 * So each is a <details>: the summary carries where, what is left, when the
 * next visit falls due and any money on it, and the card opens underneath. The
 * 346 accounts with a single plan get theirs open on arrival, which is what
 * they had before this existed.
 *
 * Built on <details> rather than script, like the cancel popover and the row
 * menu: it works with the keyboard, it works with the page half-loaded, and
 * the browser owns the state. */
/* `details.fos-card > summary` is more specific than a bare `.fos-plan >
   summary`, so this padding never applied. Matched on the element too, so the
   row that wants to be tighter than a generic card header can be. */
details.fos-plan > summary {
    list-style: none; cursor: pointer;
    display: flex; align-items: center; gap: var(--fos-s3);
    padding: var(--fos-s3) var(--fos-s4);
    padding-right: calc(var(--fos-s4) + 18px);
}
.fos-plan > summary::-webkit-details-marker { display: none; }
.fos-plan > summary:hover { background: var(--fos-cream); }
.fos-plan[open] > summary { border-bottom: 1px solid var(--fos-rule); }

/* The address, which is the row's identity and the thing being scanned down. */
.fos-plan__where { font-weight: 700; }
.fos-plan__where--missing { color: var(--fos-warn); font-weight: 600; }

/* The figures, pushed right and kept off each other. Fixed gaps rather than a
   table so the row still reads when one of them is absent — a plan with
   nothing due has no date, and most have no money outstanding. */
.fos-plan__facts {
    margin-left: auto;
    display: flex; align-items: center; gap: var(--fos-s4);
    font-size: 13px; color: var(--fos-text-muted);
    text-align: right;
}
.fos-plan__facts strong { color: var(--fos-text); }

/* The twisty. Rotated rather than swapped, so it animates and so there is one
   glyph to get right. */
.fos-plan__chev {
    flex: 0 0 auto; color: var(--fos-text-muted);
    transition: transform 120ms ease;
}
.fos-plan[open] > summary .fos-plan__chev { transform: rotate(90deg); }

@media (max-width: 720px) {
    /* Stacked, because five figures and an address do not fit a phone in one
       line and the address is the one that must survive. */
    .fos-plan > summary { flex-wrap: wrap; }
    .fos-plan__facts { margin-left: 0; width: 100%; justify-content: flex-start; }
}

/* --- A dropdown you can tick more than one thing in -------------------------
 *
 * A native multi-select needs ctrl-click for a second choice and silently
 * drops the first without it, which is how a form set to reach three people
 * reaches one. This is a <details> holding tick boxes: closed it says who is
 * on the list, open it is the one control everybody can already use twice.
 *
 * The checkboxes are real form fields, so this submits without JavaScript;
 * the script only writes the summary line.
 */
.fos-picker { position: relative; }
.fos-picker > summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--fos-s2);
}
/* Safari draws its own triangle through `display:flex` unless this is said. */
.fos-picker > summary::-webkit-details-marker { display: none; }
.fos-picker > summary::after {
    content: "\25BE";
    color: var(--fos-text-muted);
    font-size: 12px;
}
.fos-picker[open] > summary::after { content: "\25B4"; }
/* Open, the two halves are one control.
 *
 * They were a box and another box: a gap between them, a full border and four
 * rounded corners each, so the list read as something that had appeared over
 * the field rather than as the field opened up. The corners that meet are
 * squared, the borders touch, and the panel takes the summary's outline
 * colour — one shape with a line across it. */
.fos-picker[open] > summary {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
    border-color: var(--fos-rule-strong);
}
.fos-picker__body {
    /* Over the page rather than shoving it down: opening a list of twenty
       names should not move the buttons underneath out from under a cursor
       already on its way to them. */
    position: absolute;
    z-index: 30;
    left: 0;
    right: 0;
    max-height: 260px;
    overflow-y: auto;
    /* Pulled up by the border's own width so the two lines become one. */
    margin-top: -1px;
    padding: var(--fos-s1) var(--fos-s2) var(--fos-s2);
    background: var(--fos-surface);
    border: 1px solid var(--fos-rule-strong);
    border-top-color: var(--fos-rule);
    border-radius: 0 0 var(--fos-radius) var(--fos-radius);
    box-shadow: 0 10px 24px rgba(22, 32, 42, .10);
}
.fos-picker__row {
    display: flex;
    gap: 9px;
    align-items: flex-start;
    padding: 7px 6px;
    border-radius: 6px;
    cursor: pointer;
}
.fos-picker__row:hover { background: var(--fos-bg); }
.fos-picker__row input { margin-top: 3px; width: 17px; height: 17px; flex: 0 0 auto; }
.fos-picker__row span { display: block; }

/* One line of a rule — a condition, or a thing the rule does.

   These ran together. Every field is a labelled box in a column, so two
   conditions stacked read as one long list of eight fields with nothing saying
   where the first ends: the page said "This question / Was answered / That
   answer / This question / Was answered / That answer" and left somebody to
   work out the grouping from the repetition.

   So each is a bounded block with its own ground, and the strip along the top
   says what it is. On the second condition onwards that word is the join —
   "and" or "or", whichever the rule is set to — which is the one piece of
   information the page had nowhere to put and the reader most needs. */
.fos-ruleline {
    position: relative;
    background: var(--fos-surface-alt);
    border: 1px solid var(--fos-rule);
    border-radius: var(--fos-radius);
    padding: var(--fos-s3);
    margin-bottom: var(--fos-s3);
}
.fos-ruleline__top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--fos-s3);
    margin-bottom: var(--fos-s2);
    min-height: 26px;
}
.fos-ruleline__no {
    font-size: 11.5px;
    font-weight: 600;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--fos-text-faint);
}
/* Enough of a target to hit, and quiet enough not to be the first thing the
   eye lands on inside a block full of the fields that matter. */
.fos-ruleline__drop {
    flex: 0 0 auto;
    line-height: 1;
    padding: 3px 9px;
    color: var(--fos-text-faint);
}
.fos-ruleline__drop:hover {
    color: var(--fos-danger);
    border-color: var(--fos-danger);
}
.fos-ruleline__fields {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
    gap: var(--fos-s3);
    /* Top, not bottom.
       "when the answer is any of" swaps the value box for a list four rows
       tall, and bottom-aligning made the two short dropdowns sink to the foot
       of it — so their labels ended up level with the *bottom* of the list
       while its own label sat at the top, and the row read as three columns
       that had come loose from each other. */
    align-items: start;
}
.fos-ruleline__fields .fos-field { margin: 0; }

/* A condition is a sentence: this question, answered like this, with that.
   The question gets the full width because it is the only one of the three
   whose wording is not ours — "4. What is inside the ho" is what a real
   question looks like in a third of a panel. */
.fos-ruleline__fields--condition { grid-template-columns: repeat(2, 1fr); }
.fos-ruleline__fields--condition > :first-child { grid-column: 1 / -1; }

/* The form builder, side by side.

   What the form asks on the left, when it asks it on the right. One column
   meant writing a rule was a scroll past every question to reach the panel and
   a scroll back to check which question you meant.

   `start`, not `stretch`: the two columns are different lengths and always
   will be, and a rules panel stretched to the height of sixty questions is a
   card with forty centimetres of nothing under it.

   One column again below 1100px. A rule panel a third of the width of a laptop
   is a worse answer than a scroll. */
/* The builder is not a single-task screen and does not take that width. Its
   own wrapper rather than the shared one, so nothing else moves. */
.fos-build-page { max-width: 1400px; margin-inline: auto; }

.fos-build {
    display: grid;
    /* Not an even split. The left column carries the question list — six
       columns of it, one of them a whole sentence — and the right carries
       rules, which are short lines. Even halves gave the sentence a third of
       the space it needed while the rules column ran out of things to hold. */
    grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
    gap: var(--fos-s5);
    align-items: start;
}
/* The column spaces its own cards. They used to carry their own margins, which
   worked for as long as every card had one — the question editor moved above
   the list inside a plain wrapper, the wrapper had no margin, and it sat flush
   against the card above it and the table below. A gap cannot be forgotten by
   the next thing added here. */
.fos-build__col {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--fos-s5);
}

@media (max-width: 1100px) {
    .fos-build { grid-template-columns: minmax(0, 1fr); }
}

/* --- Required on -----------------------------------------------------------
 *
 * Checkboxes rather than a <select multiple>, because a multi-select has no
 * way *off* that anybody finds. Once a team was picked, clicking another only
 * moved the selection; getting back to none needed a cmd-click on the last one
 * still lit, which nobody discovers and nothing on the page says. The office
 * reported it as "there is no way to undo making a form required", which is
 * exactly what it looks like from the outside.
 *
 * Same field names, so the view is unchanged: unticking everything posts
 * nothing, and `.set([])` clears it.
 */
.fos-checklist {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px 8px;
  border: 1px solid var(--fos-line);
  border-radius: var(--fos-radius);
  background: var(--fos-surface);
  max-height: 190px;
  overflow-y: auto;
}
.fos-checklist .fos-check {
  display: flex;
  gap: 8px;
  align-items: center;
  font-size: 13.5px;
  padding: 3px 2px;
  cursor: pointer;
  margin: 0;
}
.fos-checklist .fos-check input { margin: 0; flex: none; }
.fos-checklist .fos-check:hover { background: var(--fos-hover, rgba(0,0,0,.03)); }

/* --- Pickers laid out side by side ----------------------------------------
 *
 * A grid already spaces its children with `gap`; `.fos-field`'s own bottom
 * margin lands on top of it and the columns end up sitting in twice the room
 * they were given. Same reason the row under one of these grids looked
 * stranded — 16px of field margin, then 20px of section margin, for a gap
 * nobody chose.
 */
.fos-pick-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: var(--fos-s4);
    margin-top: var(--fos-s3);
}
.fos-pick-grid > .fos-field { margin-bottom: 0; }

/* Small print sets tighter than body copy. At 11.5px the inherited 1.5 leaves
 * the lines further apart than the letters are tall, which reads as three
 * separate remarks rather than one sentence that wrapped. */
.fos-faint, .fos-help { line-height: 1.4; }

/* --- The question list on a form -------------------------------------------
 *
 * Eighty-three rows that all look the same is not a list somebody can hold in
 * their head. A section is the one row here that is not a question — it is the
 * thing the next eight belong to — and it now reads that way rather than
 * sitting in the same weight as everything under it.
 */

/* The band. Tinted, ruled above, and the name set in the page's own label
 * style so it reads as structure rather than as content. */
.q-section > td {
    background: var(--fos-surface-alt);
    border-top: 2px solid var(--fos-rule-strong);
    padding-top: 13px;
    padding-bottom: 11px;
}
.fos-table tbody tr.q-section:first-child > td { border-top: none; }

.q-section__name {
    font-weight: 700;
    font-size: 12px;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--fos-text);
}

/* Questions sit *under* their heading rather than beside it. A small indent
 * does what a nested list would, without a second level of markup to keep in
 * step with the drag-to-reorder. */
.q-row > td:nth-child(2) { padding-left: 18px; }

.q-text { font-weight: 500; }

/* "optional" was the same grey as the answer options underneath it, so it read
 * as the first of them. */
.q-optional {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--fos-text-faint);
    margin-left: 6px;
}

.q-choices {
    font-size: 12px;
    color: var(--fos-text-faint);
    margin-top: 3px;
    line-height: 1.45;
}

/* A stoplight shows three lamps instead of sixty characters of prose. The
 * words are always the same three; the colours are the thing worth seeing. */
.q-lamp {
    display: inline-block;
    width: 8px; height: 8px;
    border-radius: 50%;
    margin-right: 2px;
    vertical-align: -1px;
}
.q-lamp--green  { background: #0F7B5C; }
.q-lamp--yellow { background: #C77700; }
.q-lamp--red    { background: #B3261E; }

.q-kind { white-space: nowrap; font-size: 12.5px; }

/* Always vs by a rule. The quiet one is the common case — most questions are
 * always asked, and a column shouting it on every row is a column nobody
 * reads. */
/* The question form, once it has been carried into the row it is editing.
 *
 * Doubled class for the same reason the flush modifier is doubled: this has to
 * beat `.fos-card` wherever it lands, including under the mobile override
 * further down the file.
 *
 * The row opening, not a card arriving. It was a white panel with an edge and
 * a shadow, which reads as a thing that has landed on top of the list and has
 * to be dismissed — and it detached itself from the question it belongs to by
 * exactly the height of its own margin. Sharing the highlighted row's ground
 * and sitting flush against it makes the two one block: this question, and
 * what you are changing about it.
 *
 * The inputs stay white and that is the point of the wash rather than an
 * argument against it — on this ground a field reads as a field. */
.fos-card--inline.fos-card--inline {
    background: var(--fos-open);
    border: 0;
    border-bottom: 1px solid var(--fos-rule-strong);
    border-radius: 0;
    box-shadow: none;
    padding: 0 var(--fos-s5) var(--fos-s5) var(--fos-s5);
    margin: 0;
}
/* No border on the cell. `.fos-table td` gives every cell a bottom rule and
   the panel draws its own to close the block, so the two stacked into a double
   line under the buttons — one light, one strong, a pixel apart. The panel's
   is the one that means something. */
.q-editing > td {
    padding: 0 !important;
    background: var(--fos-open);
    border-bottom: 0;
}

/* The row it belongs to. Same ground as the panel and no line between them, so
   the question and its editor read as one thing rather than two. */
tr.q-open > td {
    background: var(--fos-open);
    border-bottom: 0 !important;
}
tr.q-open .q-text { font-weight: 700; }

/* The title inside the panel. It had a rule under it separating it from the
   fields, which was a third horizontal line in four rows of them. */
.fos-card--inline .fos-card__title {
    padding-top: var(--fos-s4);
    margin-bottom: var(--fos-s4);
    border-bottom: 0;
}

/* Inside the panel: the dropdown and the switches side by side.
 *
 * Stacked, the editor was a column of full-width fields and the panel ran
 * taller than the screen — which is a strange thing for a box whose whole
 * purpose is to keep you next to the row you clicked. */
.fos-qedit-split {
    display: grid;
    grid-template-columns: minmax(0, 19rem) minmax(0, 1fr);
    gap: var(--fos-s4) var(--fos-s5);
    align-items: start;
    border-top: 1px solid var(--fos-rule);
    margin-top: var(--fos-s3);
    padding-top: var(--fos-s4);
}
@media (max-width: 820px) {
    .fos-qedit-split { grid-template-columns: minmax(0, 1fr); }
}

/* The rules named inside the question editor. One per line, the sentence on
   the left and the button that opens it on the right, so a list of three
   reads down the words rather than around the buttons. */
.fos-qrules { display: flex; flex-direction: column; gap: 6px; }
.fos-qrule {
    display: flex;
    gap: var(--fos-s3);
    align-items: center;
    justify-content: space-between;
    background: var(--fos-surface);
    border: 1px solid var(--fos-rule);
    border-radius: 8px;
    padding: 7px 9px 7px 11px;
    font-size: 12.5px;
    line-height: 1.4;
}
.fos-qrule > span { min-width: 0; }

/* Children spaced by one gap rather than by a margin each of them has to
 * remember. A page whose spacing lives on its items is a page where the next
 * item added lands flush against its neighbour — which is what happened to the
 * finished-form page when the address box moved above the readout: five
 * children, gaps of 20, 20, 16, 16 and 0. */
.fos-stack { display: flex; flex-direction: column; gap: var(--fos-s5); }
/* The stack owns the vertical spacing outright. Anything that also carries a
   margin of its own gets that margin *added* to the gap — which is how the
   same page ended up with 40 above two of its cards and 20 above the rest,
   from margins those elements carry for the pages that do not stack them. */
.fos-stack > * { margin-top: 0; margin-bottom: 0; }

/* --- A rule, opened for editing ---------------------------------------------
 *
 * The same idea as the question row: the card holding the rule opens, rather
 * than a panel arriving somewhere else. The form sits inside the card it
 * belongs to, ruled off from the rule's own summary above it, so the two read
 * as one thing — this rule, and what you are changing about it.
 */
.fos-rulecard--open {
    background: var(--fos-open);
    border-color: var(--fos-rule-strong);
}
.fos-rulecard__form {
    border-top: 1px solid var(--fos-rule-strong);
    margin-top: var(--fos-s4);
    padding-top: var(--fos-s4);
}

/* The box for a new rule keeps the separator it had above the list. It is off
   while the form is away being an editor, because there it has a rule of its
   own above it. */
#rule-form-home > #rule-form {
    margin-bottom: var(--fos-s4);
    padding-bottom: var(--fos-s4);
    border-bottom: 1px solid var(--fos-rule);
}

/* --- The switches on a question ------------------------------------------
 *
 * Three facts of the same kind, so one shape for all three: the tick, the
 * name, and why you would touch it, on one line. They were three separate
 * fields with three headings and three paragraphs, two of them sharing a grid
 * with a dropdown — which made a row whose halves were three lines and one.
 *
 * The name column is fixed rather than sized to content, so the reasons start
 * at the same place down the list and the eye has one edge to follow. */
.fos-switches { display: grid; gap: 9px; align-content: start; }
.fos-switch {
    display: grid;
    grid-template-columns: auto 10.5rem minmax(0, 1fr);
    gap: 0 10px;
    align-items: baseline;
    cursor: pointer;
}
/* In the panel the switches share the width with the dropdown, so the reason
   goes under the name rather than into a column three words wide. */
.fos-qedit-split .fos-switch { grid-template-columns: auto minmax(0, 1fr); }
.fos-qedit-split .fos-switch__why { grid-column: 2; }
.fos-switch input { margin: 0; }
.fos-switch__name { font-weight: 600; }
.fos-switch__why {
    color: var(--fos-text-faint);
    font-size: 11.5px;
    line-height: 1.45;
}

/* Narrow: the reason drops under the name rather than squeezing into a column
   two words wide. The tick keeps its own column so the name stays beside it. */
@media (max-width: 700px) {
    .fos-switch { grid-template-columns: auto minmax(0, 1fr); }
    .fos-switch__why { grid-column: 2; }
}

/* Required is the ordinary state and reads as plain text; optional is the
   exception and is the one that gets softened. The pair has to be legible at
   a glance down a column of eighty-three rows. */
.q-req { font-weight: 600; color: var(--fos-text-muted); }
.q-req--optional { font-weight: 500; color: var(--fos-text-faint); }
/* Not applicable, rather than a quiet no. A dash reads as "this column has
   nothing to say about this row", which is the truth for a note. */
.q-req--none { font-weight: 500; color: var(--fos-rule-strong); }

.q-when { color: var(--fos-text-faint); }
.q-when--rule {
    color: var(--fos-text-muted);
    font-weight: 600;
}
