/* ======================================================================
   The Daclaria design system (SAAS-49).

   Palette: near-neutral ink, with one brand hue. Colour appears in exactly two places — the
   one primary action, and status. That is the whole rule, and it is what makes the freshness
   ladder legible: a reader never has to work out whether a coloured thing is a status or just
   decoration. Nothing here may introduce a third use of colour.

   Amended by SAAS-86 (ADR 0020): the chrome and the action are now Daclaria teal, where this
   file previously said "the brand is achromatic". The count did not change — one action hue,
   three status hues, achromatic content — only which hue the action wears, and the fact that
   the chrome now wears it too. See the --teal block below for the argument and its cost.

   Both themes are authored. The previous sheet derived its greys from `currentColor` mixes,
   which is how dark mode arrived without ever being designed — and how the report dropdowns
   ended up white-on-white. Every colour below is a chosen value in both themes.

   ----------------------------------------------------------------------
   This :root block is the product's *only* token declaration (SAAS-52).

   The Operator realm has a sheet of its own — operator.css, loaded beside this one on every page of
   both realms — and the shape of the shared layer is that the tokens are declared here and
   nowhere else. operator.css names no colour of its own: no hex, no rgb(), no named colour, and no
   mix off `currentColor`. So there is no ink colour to change in one realm and not the other,
   because there is only one place either realm can be changed from.

   That is a rule someone can break by accident, so it is a test rather than a habit:
   OperatorStylesheetTests reads both files and fails if operator.css declares a token, writes a colour
   literal, or references a `--token` this block does not define — the last of which would
   otherwise render as nothing at all and be visible only to whoever next looked at the screen.

   The alternative considered was a third sheet, tokens.css, imported by both. Rejected: it moves
   rules that already depend on these tokens for no gain the test above does not already give,
   and this file had been rewritten three times in one day (SAAS-49, 50, 51) with two of those
   slices still in review. Additive was the shape with the least chance of taking the Owner realm
   down with it.
   ====================================================================== */

@font-face {
    /* Inter, self-hosted. Chosen over Source Sans 3 for its tabular figures and its taller
       x-height at the 12–13px the report tables run at. Variable, so one file carries every
       weight the product uses. Served from our own origin — never a CDN (SAAS-49). */
    font-family: "Inter";
    src: url("fonts/inter-variable.woff2") format("woff2");
    font-weight: 100 900;
    font-style: normal;
    font-display: swap;
}

:root {
    /* Kept: it is what makes native controls, scrollbars and the OS dropdown popup follow the
       page's theme rather than fight it. The tokens below are ours; this is for what we do not
       paint ourselves. */
    color-scheme: light dark;

    --page: #F7F8FA;
    --surface: #FFFFFF;
    --ink: #101828;
    --ink-strong: #0B1120;
    --muted: #667085;
    --border: #E4E7EC;
    --border-strong: #D0D5DD;

    /* The brand carries one hue, and it is Daclaria teal (ADR 0020, SAAS-86).

       This amends a rule this file used to state — "the brand is achromatic" — and it is worth
       saying exactly what changed, because the old rule was load-bearing and the new one keeps
       the part that mattered. Colour is still spent on exactly two things: the one primary
       action, and status. Content is still achromatic, emphasis is still a wash of ink and a
       change of weight, and the freshness ladder still owns --ok/--warn/--bad alone. What moved
       is *which* hue the single action wears, not *how many* hues the product has.

       The rejected option had the better argument on paper: teal in the chrome only, leaving the
       action blue, keeps the maximum distance between the brand and a green/amber/red ladder,
       which is the thing the original rule warned about. Three treatments were built and compared
       side by side, and Darius took the third. Written down because the paper argument will be
       re-made by whoever reads the old rule next.

       The cost is measured, not assumed: teal sits at 4.95:1 against white where slate was
       14.68:1 and the blue action 8.72:1. All pass AA, and the headroom above the floor is now
       thin enough that any future tint of either surface has to be re-measured rather than
       eyeballed. */
    --teal: #0E7C86;
    --teal-deep: #0A616A;
    --teal-wash: #E6F4F5;

    /* The ink that reads on --teal-wash. It is a separate token because the two themes disagree
       about which end of the teal is legible on their own wash — deep on a pale tint here,
       lifted on a translucent one in dark. */
    --nav-active-ink: var(--teal-deep);

    --brand: var(--teal);
    --on-brand: #FFFFFF;

    /* The single action colour. One filled button per screen, at most. */
    --action: var(--teal);
    --on-action: #FFFFFF;
    --action-hover: var(--teal-deep);

    /* Status. These three belong to the freshness ladder and to stated failures, and to
       nothing else. */
    --ok: #1A7F37;
    --warn: #9A6700;
    --bad: #B42318;

    /* Custom-vs-Standard report scope. Not a status — deliberately off the status hues. */
    --scope-custom: #6941C6;

    /* A person's own pin on a report (SAAS-133) — the star on the report list, lit. Not a status
       either, and NOT --warn, which is the near thing it looks like: --warn measures 4.32:1 against
       --teal-wash, and --teal-wash is the selected row a lit star most often sits on, so reusing it
       would have shipped a state that fails AA in exactly the place it matters. This hue measures
       5.43:1 on --surface, 5.07:1 on a hovered row and 4.81:1 on --teal-wash; the dark one is
       11.71 / 10.67 / 9.12. Measured on the shipped tokens, not taken from the mock. */
    --star: #B54708;

    /* ------------------------------------------------------------------
       BRANCH IDENTITY ON THE MERGED OWN CALENDAR (SAAS-151, ADR 0020 amended).

       This is the one sanctioned use of colour beyond "the action, and status", and Darius took
       it knowingly from a rendered comparison. The reason it is worth the exception: on a doctor's
       merged week the clinic is what the page is scanned by, and until this slice it was carried
       by repeating the whole display name on every block — which ate the block before the
       patient's name appeared. A hue plus a short code is the smallest thing that can carry it.

       THE HUE IS A SCANNING AID; THE CODE IS THE IDENTITY, and the two are never shown apart.
       That is not a nicety — there are only three hue bands genuinely free here once teal (the
       action and the chrome), green/amber/red (the status ladder) and burnt orange (--star) are
       reserved. So the palette is three hues at two values each, and past six Branches it repeats.
       A repeated hue is then a repeated *colour* under two different codes, which reads as "these
       two are not the same clinic" rather than as an ambiguity — and the legend prints both.

       MEASURED, not eyeballed, against both themes' surfaces. Light: 6.62 / 6.99 / 6.03 / 7.57 /
       7.67 / 7.06 against --surface, and 6.23 / 6.58 / 5.67 / 7.12 / 7.22 / 6.64 against --page.
       Dark: 7.52 / 8.48 / 8.76 / 8.76 / 8.91 / 9.27 against --surface. The floor that matters is
       4.5:1, because the code is 11px text and 11px bold is not "large". The swatch and the
       block's own edge clear the 3:1 non-text floor several times over as a consequence. */
    --branch-1: #2E5AAC;
    --branch-2: #7A3E9D;
    --branch-3: #A83A6B;
    --branch-4: #3E5578;
    --branch-5: #5F4A78;
    --branch-6: #7A4A60;

    /* Code (SAAS-52). The Operator realm reads and writes SQL, so a monospace stack and a surface to
       set it on are product-wide roles rather than Operator-only ones — `code` below already used a
       stack, and operator.css had grown a second one that differed by a font ("Cascadia Code" against
       "Cascadia Mono"), which is exactly the two-sheets-one-product problem in miniature.

       System stack, no webfont: 0 KB over the wire. Inter is self-hosted at ~110 KB and that is
       already a real cost on a filtered Indonesian connection; a second face to render SQL that
       only Daclaria staff ever see would be spending a clinic's bandwidth on our own tooling. A
       CDN is never an option here for anything.

       Cascadia Mono before Cascadia Code deliberately: both ship on Windows 11, and Code is the
       ligature cut — `<=`, `>=` and `!=` fuse into single glyphs, which is a preference in an
       editor and a hazard in a body of SQL somebody is auditing against a captured corpus. */
    --mono: ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

    /* The surface a block of code sits on. A step down from --surface rather than up, so a code
       panel reads as recessed into the page — the same idiom in both themes, not the same value. */
    --code-surface: #F2F4F7;

    --row-hover: rgba(16, 24, 40, .035);
    --focus-ring: color-mix(in srgb, var(--action) 55%, transparent);

    /* Retained so the rules written against them keep working — but now pointing at authored
       colours rather than at a mix of whatever text colour happened to be inherited. */
    --line: var(--border);

    /* Prose only. There is deliberately no shell-width token: the application spans the
       viewport, and capping it is what put the sidebar in the middle of the screen. */
    --measure: 68ch;

    /* The masthead band's own three values (SAAS-117): what a link's hover and current states are
       washed with, and what draws an outline on the band.

       ⚠ MEASURED, AND THE OBVIOUS ANSWER IS WRONG. The rendered comparison Darius ruled from
       marked the current entry with a *lighter* wash — white at 14% — which is what a coloured
       masthead usually does. It fails here: --brand is teal at 4.95:1 against white, so lifting
       the band underneath the text takes the text down with it. White on that composite measures
       3.85:1, under the 4.5:1 floor, on the one entry that has to be legible. The wash therefore
       cuts *down* into the band rather than up out of it: white reads 6.95:1 on the current wash
       and 5.85:1 on hover — both better than the plain band they sit on.

       One pair for both themes, deliberately. They are alpha over --brand, and --brand is the
       token that already differs per theme — dark's #0B4E56 is darker still, and the same two
       values measure 10.15:1 and 9.09:1 there. Light is the constrained theme; measure there and
       dark follows.

       The line is white rather than black for the opposite reason: it outlines the account chip,
       which is a boundary rather than text, and 68% white clears the 3:1 non-text floor at 3.16:1
       where a dark line on a mid-teal band would not. */
    --masthead-hover: rgba(0, 0, 0, .10);
    --masthead-current: rgba(0, 0, 0, .20);
    --masthead-line: rgba(255, 255, 255, .68);

    /* ------------------------------------------------------------------
       The craft layer (SAAS-86, ADR 0020).

       Everything above this line is a hue. Until this slice that was the *whole* token block —
       no spacing scale, no type scale, no radius, no elevation. Rhythm, proportion and density
       had therefore never been designed: every component picked its own value, and the sheet
       had drifted into eighteen distinct font sizes between 10.9px and 21.6px, most of them
       separated by less than a pixel and none of them chosen against the others.

       The reason that happened is legible in the tickets and worth keeping in view. SAAS-49
       through SAAS-55 verified contrast ratios, alignment mismatches and scrollWidth — exactly
       the things a machine can count, and not one of which is proportion. No agent can sign in
       to this product, so the axes that got attention were the measurable ones. These four
       scales are the answer to that, and they are deliberately short: a closed set of steps is
       what makes a component's size a decision rather than a typing accident.
       ------------------------------------------------------------------ */

    /* Space. Eight steps, each roughly 1.5x the last at the small end where the eye is most
       sensitive, doubling at the top where it is not. */
    --s1: 4px;
    --s2: 8px;
    --s3: 12px;
    --s4: 16px;
    --s5: 24px;
    --s6: 32px;
    --s7: 48px;
    --s8: 64px;

    /* Type. Seven steps. --t-body is the density ruling: 14px, with a 42px table row. Roomy
       (15.5/48) and compact (12.5/36) were both built and refused. */
    --t-micro: 11px;
    --t-small: 12.5px;
    --t-body: 14px;
    --t-lead: 15.5px;
    --t-h3: 17px;
    --t-h2: 21px;
    --t-h1: 27px;

    /* Radius. Three steps: controls, panels, cards. The pill (999px) and the circle (50%) are
       shapes rather than steps and stay written out where they are used. */
    --r-sm: 6px;
    --r-md: 10px;
    --r-lg: 14px;

    /* Elevation. Two levels, both cheap, both tinted with the ink rather than pure black — a
       neutral-black shadow on a warm-neutral page reads as grime. */
    --e1: 0 1px 2px rgba(16, 24, 40, .05);
    --e2: 0 1px 3px rgba(16, 24, 40, .06), 0 6px 16px -6px rgba(16, 24, 40, .10);

    /* The table row's vertical padding, so density is changed in one place rather than in every
       table in the product. 10px + 10px + a 21px line box is the 42px row.

       The prototype paired this with a --cell-x of 24px. That step is deliberately not carried
       over: there the table was the page, while here every table sits inside a block that
       already pays its own horizontal padding, so a second 24px inside the cell would indent the
       first column past its own heading and widen the widest thing in the product for nothing.
       The columns keep --s3, and the one place a wider gutter is genuinely needed — between two
       adjacent right-aligned number columns — asks for it by name. */
    --row-y: 10px;

    /* HOW FAR A LANDED ROW SITS BELOW THE TOP OF THE VIEWPORT (SAAS-170).

       The masthead is sticky at this width, so a browser that scrolls a row exactly to the top of
       the page parks it underneath the band — the same "I still have to scroll" this slice exists
       to remove, one screen higher up. This is the band's own height plus a gap: it is a slim one
       since ADR 0027, --s2 of padding either side of a ~32px account chip, and the step above that
       leaves the row clear of the band's edge rather than tight against it.

       A token rather than a literal because the narrow width says a different thing (0 — the band
       is not sticky there), and one number written in two places is one number that drifts. */
    --landing: var(--s8);
}

/* The guard on this selector is what lets the page be forced back to light, and it is the whole
   mechanism behind the printed document's light theme (SAAS-95).

   `:root:not([data-theme="light"])` rather than a bare `:root`: print.js stamps
   `data-theme="light"` on <html> before the browser paints a print, and removes it after. With
   that attribute present this block simply does not apply, so every token falls back to the light
   values already declared above — no second palette, no print-only colours, and nothing to keep in
   step across two places. A print stylesheet that re-declared the light values would be exactly
   the divergence SAAS-52 removed from this product.

   It also means a browser that honours `prefers-color-scheme` in its print output cannot put a
   near-black page onto white paper, which is the failure the ruling names. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --page: #0A0B0D;
        --surface: #14161A;
        --ink: #ECEFF3;
        --ink-strong: #FFFFFF;
        --muted: #98A2B3;
        --border: #23262B;
        --border-strong: #2F343B;

        /* The brand hue survives into dark, and the masthead keeps it (ADR 0020, SAAS-86).

           --brand was #15171B here: a near-black that receded into the page. Teal is instead
           lifted to a deep #0B4E56 — present, but not a lightbar across a #0A0B0D page. The
           alternative was built and rejected, and the reason is worth recording: a brand hue
           that does not survive both themes makes dark mode a differently-branded product, and
           leaves the hue on exactly one button. The wordmark reads at 8.13:1 against it.

           --teal is lifted to the same relative luminance the shipped theme already used for
           #8FB0F5, so the one action reads with the weight it always had, with dark ink on it —
           the flip to dark ink is a rule this theme already had, not a new one.

           Dark is the *safer* theme at every point, which is the opposite of the instinct: the
           action clears 8:1 here against 4.95:1 in light. So light is the constrained theme.
           Measure every future colour decision there and dark follows for free, not the
           reverse. */
        --teal: #4FC3CE;
        --teal-deep: #6BD2DB;
        --teal-wash: rgba(79, 195, 206, .14);
        --nav-active-ink: var(--teal);

        --brand: #0B4E56;
        --on-brand: #ECEFF3;

        --action: var(--teal);
        --on-action: #062024;
        --action-hover: var(--teal-deep);

        /* Lifted for contrast against a near-black page — the same three meanings, not the
           same three values. A 4.5:1 green on white is unreadable on #0A0B0D. */
        --ok: #6BD98A;
        --warn: #E8B341;
        --bad: #F98A80;

        --scope-custom: #B69DF0;

        /* Amber rather than the light theme's burnt orange, for --ok/--warn/--bad's reason: a
           4.5:1 hue on white is unreadable on #0A0B0D. Measured above. */
        --star: #FEC84B;

        /* The Branch hues, lifted (SAAS-151). Same three bands and the same two-values-per-band
           system as the light theme, mirrored: where light darkens the second triad toward slate,
           dark desaturates it toward the same slate from above. Keeping the *structure* identical
           is what lets a doctor learn the key once — the second blue is the muted blue in both
           themes, not a different relationship. All six measure 7.5:1 or better on --surface. */
        --branch-1: #7CA8F0;
        --branch-2: #C4A2F5;
        --branch-3: #E79FC4;
        --branch-4: #9FB6D8;
        --branch-5: #C0AEDC;
        --branch-6: #D9AEBE;

        /* Still a step *down* from --surface (#14161A) on a page already at #0A0B0D. The border
           carries most of the panel's edge here; the fill only has to stop the code reading as
           loose text on the page. */
        --code-surface: #101318;

        --row-hover: rgba(255, 255, 255, .04);

        /* Elevation is the one part of the craft layer that cannot carry its light values over.
           A 5%-ink shadow is invisible on a #0A0B0D page, so depth in dark comes from a denser
           black instead — the same two levels, not the same two values, which is the rule the
           rest of this block already follows. */
        --e1: 0 1px 2px rgba(0, 0, 0, .5);
        --e2: 0 1px 3px rgba(0, 0, 0, .55), 0 6px 16px -6px rgba(0, 0, 0, .65);
    }
}

body {
    margin: 0;
    font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
    font-size: var(--t-body);
    line-height: 1.5;
    background: var(--page);
    color: var(--ink);
}

/* --- The site frame --------------------------------------------------- */

/*
   The masthead (SAAS-117): one band across the top carrying the wordmark, the navigation and the
   account menu. It replaced a slim header over a left rail, which Darius ruled against from a
   rendered comparison on 2026-08-22.

   STICKY IN BOTH REALMS, and that is a change rather than a copy. operator.css used to make only
   the vendor header sticky, and said in as many words that it was sticky *because* that realm had
   no rail — the Owner realm's marker was the sidebar, which was already always on screen. Neither
   realm has a rail now, so this band is the only chrome there is, and it carries the one control
   that must never need scrolling to reach (SAAS-116).

   z-index 40 clears the day-end blocks and the report preview's sticky headings — those position
   against .block-scroll, their own scroll box, not against the viewport.
*/
.site-header {
    background: var(--brand);
    color: var(--on-brand);
    position: sticky;
    top: 0;
    z-index: 40;
}

/*
   The shell spans the viewport. It used to be capped at a centred 78rem, which was a mistake I
   made in SAAS-49: 78rem is a *document* width, and this is an application. Capping it put the
   navigation in the middle of the screen with dead space either side of it, and — worse — clipped
   the right-hand columns off the widest reports while several hundred pixels sat empty beside
   them.

   Prose is still measured. `.lead` and the report descriptions keep `--measure`, because a
   sentence 1900px wide is unreadable in a way a table is not.
*/
.masthead-inner {
    box-sizing: border-box;
    padding-inline: var(--s5);
    padding-block: var(--s2);
    display: flex;
    align-items: center;
    gap: var(--s5);

    /*
       IT IS A SLIM BAND SINCE SAAS-162 (ADR 0027). Navigation left it for the rail, so what is left
       is the wordmark at one end and the Tenant and account chip at the other — one row at every
       width the masthead was measured at.

       The wrap stays, and it is now insurance rather than the narrow-width answer it used to be:
       a long Tenant name beside a long email still has somewhere to go. The account block is last
       in the line and carries `margin-inline-start: auto`, so it is the half that drops when the
       two do not fit, which keeps sign-out one click away at every width (SAAS-116).
    */
    flex-wrap: wrap;
}

/* Session context and the account menu, at the far end of the band — and the whole of the second
   row when the band wraps. */
.masthead-right {
    display: flex;
    align-items: center;
    gap: var(--s3);
    margin-inline-start: auto;
    min-width: 0;
}

/* Whose data this is — session context, deliberately not part of the wordmark. Quieter than the
   brand: it is a confirmation, and an Owner already knows the answer. */
.site-tenant {
    opacity: .8;
    font-size: .95em;
    text-align: right;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The page. A flex item beside the rail again (SAAS-162), and `min-width: 0` is what keeps it one:
   a flex item's floor is its content's min-content width, so without this the widest report would
   push the rail off the screen instead of scrolling inside its own box (.tblbox, .library-scroll).

   What it gives up to the rail is 232px — 1,390 → 1,158 at 1440, 998 at 1280 — and that price was
   on the rendered frames when Darius accepted them. The picker and filter grids pay it by dropping
   from five columns to four, which needs no rule of its own: they are `repeat(auto-fit, minmax(14rem,
   1fr))` and have always answered to the width they are given. */
.site-main {
    box-sizing: border-box;
    min-width: 0;
    padding-inline: var(--s5);
    padding-block: var(--s5) var(--s8);
}

.brand {
    display: inline-flex;
    align-items: baseline;
    gap: var(--s2);
    color: inherit;
    text-decoration: none;
    white-space: nowrap;
}

.brand-name {
    font-weight: 650;
    letter-spacing: -.01em;
}

.brand-sep {
    opacity: .45;
}

.brand-qualifier {
    opacity: .85;
    font-size: .95em;
}

/* The page title takes the top of the type scale (SAAS-86). It was 1.35rem — 21.6px, the same
   step the section headings landed on — which is the undesigned proportion this slice exists to
   fix: a page whose title, its sections and its blocks all sit within 4px of each other has a
   hierarchy in the markup and none on the screen. 27 / 21 / 17 is the ladder. */
h1 {
    font-size: var(--t-h1);
    font-weight: 640;
    line-height: 1.2;
    letter-spacing: -.021em;
    color: var(--ink-strong);
    margin: 0 0 var(--s2);
}

.lead {
    color: var(--muted);
    margin: 0 0 var(--s5);
    max-width: var(--measure);
}

/* --- The shell's navigation (SAAS-117, given its rail in SAAS-162) ------ */

/* Out of sight, still in the accessibility tree. It is the table captions' rule now — the
   collapsed rail that used to depend on it is gone, and with it the argument that made it
   load-bearing: a navigation link's accessible name is simply its own text, because nothing clips
   it. The rail wraps captions rather than clipping them, so that stays true. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* =================================================================================================
   ONE INVENTORY, TWO SHAPES BY WIDTH (SAAS-162, ADR 0027).

   ShellNav draws the areas and the current area's views ONCE. This sheet lays that one markup out
   twice, and the two shapes are the two directions Darius picked from the SAAS-128 rendered
   comparison — with both widths in view, and amended the same day to take one from each:

     · Below 768px — DIRECTION B. The areas are a row under the masthead, wearing the masthead's own
       teal so they read as part of it, and the current area's views are a band beneath them.
       Nothing is hidden, there is nothing to open, and there is no <details> anywhere in this
       shell: "for mobile I like B more, can we do desktop A and mobile B?"
     · 768px and up — DIRECTION A. The same list becomes a 232px rail down the left of the page in
       BOTH realms, with the views nested under the area they belong to. That is what the masthead
       could no longer hold: an Owner's Appointments strip is nine tabs, 2,063px of caption, and it
       already wrapped to two rows on a desktop.

   The rules below are written narrow-first, so the phone shape is the base and the rail is what a
   media query adds. Every rule that differs between the two shapes is in one of those two places
   and nowhere else — which is the whole reason this is one inventory and not two.

   NO SCRIPT, NO STATE, NO PRE-PAINT. The 2026-08-22 deletion of the old rail's machinery stays
   deleted; a shape chosen by width alone has nothing to remember and nothing to restore.
   ============================================================================================== */

/* The rail and the page are siblings, so their arrangement is this container's to decide: stacked
   on a phone, side by side above the breakpoint. */
.shell-body {
    display: flex;
    flex-direction: column;
    min-height: 0;
}

/* NARROW: the masthead's own second row, and it says so by wearing --brand. The header ends and
   this begins with no gap and no line between them, which is what makes the two read as one band.
   The wrap is the same answer the masthead used to give for itself — eight captions do not fit
   375px, and rows are the only arrangement that hides none of them. */
.shell-nav {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s1);
    padding: var(--s1) var(--s4) 0;
    background: var(--brand);
    color: var(--on-brand);
}

/* Heads the rail's column. Drawn at one width only: the narrow shape's row is under the masthead
   and reads as the masthead's, where a heading would be noise. */
.nav-cap {
    display: none;
}

/* Full --on-brand, not a quieter tint, and that is measured rather than chosen — the rendered
   comparison dimmed these links and this sheet cannot afford to. --brand is teal at 4.95:1 against
   white, so there is almost no headroom: 88% white measures 4.21:1 and 92% measures 4.45:1, both
   under the 4.5:1 floor. Every entry is therefore at full strength, and what separates the current
   one is a wash and a weight rather than a difference in ink.

   44px, at BOTH widths and not only under `pointer: coarse` (ADR 0027 point 4). The phase-1
   measurement found the old .top-link at 37px, and the coarse-pointer rule did not reach it: that
   block widens buttons and submits, and these are anchors. The floor is the target's own rule here,
   and SAAS-163 pins it portal-wide. */
.nav-area {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: var(--s1) var(--s3);
    border-radius: var(--r-sm);
    color: inherit;
    text-decoration: none;
    white-space: nowrap;
}

.nav-area:hover {
    background: var(--masthead-hover);
}

/* Where you are: cut into the band rather than lifted out of it — see --masthead-current for the
   measurement that decides which way round. The weight change carries the state for anyone who
   cannot see the tint at all. */
.nav-area[aria-current="page"] {
    background: var(--masthead-current);
    font-weight: 600;
}

/* THE SECOND LEVEL. Narrow: a band of its own under the whole row of areas, full-bleed and in the
   page's own colours so it reads as one level down rather than as more masthead.

   `order` is what puts it there. It renders in the markup directly after the area it belongs to —
   which is where the rail wants it — and one order value moves it to the end of the flex line
   without the shell having to draw it twice. The width and the negative margin are the full bleed:
   the band spans the viewport while the areas above it keep the masthead's gutter. */
.nav-second {
    order: 1;
    flex: 0 0 auto;
    box-sizing: border-box;
    width: calc(100% + var(--s4) * 2);
    margin-inline-start: calc(var(--s4) * -1);
    margin-block-start: var(--s1);
    padding-inline: var(--s4);
    display: flex;
    align-items: center;
    gap: var(--s4);
    background: var(--surface);
    color: var(--ink);
    border-bottom: 1px solid var(--border);
}

/* AN AREA WITH NO SECOND LEVEL PAYS NO BAND FOR ONE, and this is the one rule that keeps direction
   B's measured cost off six of the Owner's eight areas: B was measured at 46px on an area with
   nothing in the band, and that is 46px of chrome saying only what the masthead already said.
   The shell cannot know whether a page filled the outlet — a section is content, not a signal — so
   the emptiness itself is the test. */
.nav-second:not(:has(.surface-views)) {
    display: none;
}

/* Which area's views these are. The narrow band needs it — a row of view names under a row of area
   names says nothing about which belongs to which — and the rail does not, because there the views
   sit directly under the link that names them. */
.nav-second-name {
    flex: 0 0 auto;
    font-size: var(--t-small);
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--muted);
    white-space: nowrap;
}

/* THE VIEWS THEMSELVES (SAAS-74's strip, SAAS-152's second copy, moved into the shell by SAAS-162).
   The page still builds them; only where they land changed, so every address they carry — the
   filters that survive a crossing, the day on a calendar tab — is untouched.

   Narrow, they are the tab idiom a reader already learned on Billing, one level up: an underline
   marks the current view, achromatically as everything about "which of these am I reading" is in
   this palette. 44px like the areas above them. */
.surface-views {
    min-width: 0;
}

.surface-views ul {
    display: flex;
    flex-wrap: wrap;
    gap: 0;
    margin: 0;
    padding: 0;
    list-style: none;
}

.surface-views a.view {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 var(--s3);
    border-bottom: 2px solid transparent;
    color: var(--muted);
    text-decoration: none;
    font-size: var(--t-body);
    white-space: nowrap;
}

.surface-views a.view:hover {
    color: var(--ink);
}

.surface-views a.view.current {
    border-bottom-color: var(--teal);
    color: var(--ink-strong);
    font-weight: 650;
}

/* --- WIDE: the rail (direction A) --------------------------------------- */

@media (min-width: 768px) {
    .shell-body {
        flex-direction: row;
        align-items: stretch;
    }

    /* 232px, which is the width the frames were accepted at. It is not sticky, and that is a stated
       choice rather than an oversight: the masthead above it is, and it carries the one control
       that must never need scrolling to reach (SAAS-116). A sticky rail would have to know that
       band's height to sit under it, and a wrong answer hides the top of the menu behind it. So the
       rail scrolls with the page, exactly as the view strip it absorbed always did. */
    .shell-nav {
        display: block;
        flex: 0 0 232px;
        width: 232px;
        box-sizing: border-box;
        padding: var(--s4) var(--s3);
        background: var(--surface);
        color: var(--ink);
        border-right: 1px solid var(--border);
    }

    /* The other item in the row, and it takes what is left. Only here: below the breakpoint the
       shell body is a column, where `flex-basis: 0` would make the page's height a thing the
       browser has to argue itself back out of. */
    .site-main {
        flex: 1 1 0;
    }

    .nav-cap {
        display: block;
        padding: 0 var(--s2) var(--s2);
        font-size: var(--t-micro);
        font-weight: 700;
        letter-spacing: .07em;
        text-transform: uppercase;
        color: var(--muted);
    }

    /* A column, so a long caption wraps rather than widening the rail. `Calendar — Klinik Kidz
       Dental Sehati` is the case that decides this, and its wording is SAAS-164's to shorten. */
    .nav-area {
        display: flex;
        white-space: normal;
    }

    .nav-area:hover {
        background: var(--row-hover);
    }

    /* Teal on the page's own surface, where the band's cut-in wash has nothing to cut into. */
    .nav-area[aria-current="page"] {
        background: var(--teal-wash);
        color: var(--nav-active-ink);
        font-weight: 650;
    }

    /* Nested under its area, and the rule down the left is what says so — indent alone would read
       as a gap. */
    .nav-second {
        display: block;
        width: auto;
        margin: var(--s1) 0 var(--s2) var(--s4);
        padding: 0 0 0 var(--s3);
        background: none;
        border-bottom: 0;
        border-left: 1px solid var(--border-strong);
    }

    .nav-second-name {
        display: none;
    }

    .surface-views ul {
        display: block;
    }

    .surface-views a.view {
        display: flex;
        border-bottom: 0;
        border-radius: var(--r-sm);
        font-size: var(--t-small);
        white-space: normal;
    }

    .surface-views a.view:hover {
        background: var(--row-hover);
    }

    /* The tab's underline has no edge to sit on in a column, so the marking becomes the wash the
       pager and the table rows already use for "this one". */
    .surface-views a.view.current {
        background: color-mix(in srgb, var(--ink) 9%, transparent);
        color: var(--ink-strong);
        font-weight: 650;
    }
}

/* --- The account menu (SAAS-116, SAAS-117) ----------------------------- */

/* A <details>, so it opens, closes, takes focus and announces its own expanded state with no
   script of ours — see MainLayout. `position: relative` is what the popover hangs off. */
.account-menu {
    position: relative;
}

.account-menu > summary {
    /* The marker goes in both engines' spellings. Without this the browser draws its own triangle
       beside the chevron we authored. */
    list-style: none;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    min-width: 0;
    padding: var(--s1) var(--s2);
    border: 1px solid var(--masthead-line);
    border-radius: 999px;
    color: inherit;
    font-size: var(--t-small);
}

.account-menu > summary::-webkit-details-marker {
    display: none;
}

.account-menu[open] > summary {
    background: var(--masthead-current);
}

/* A long email is the common case, not the exception, and it must not push the chevron off the
   band. */
.account-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The initials chip. Monospace, so two letters and one letter occupy nearly the same width and the
   summary does not change size between an Operator and an Owner. */
.mono-chip {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.5rem;
    height: 1.5rem;
    padding-inline: 4px;
    box-sizing: border-box;
    border: 1px solid var(--masthead-line);
    border-radius: 999px;
    font-family: var(--mono);
    font-size: var(--t-micro);
}

/* The popover leaves the brand band and lands on the page's own surface — it is content, not
   chrome, and white-on-teal menu items would read as part of the header. Right-aligned to the
   summary, because the summary is at the right edge of the band. */
.menu-pop {
    position: absolute;
    right: 0;
    top: calc(100% + var(--s2));
    z-index: 50;
    width: 16rem;
    box-sizing: border-box;
    padding: var(--s3);
    background: var(--surface);
    color: var(--ink);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    box-shadow: var(--e2);
}

/* overflow-wrap, not ellipsis: inside the menu there is room to break a long address over two
   lines, and this is the one place the whole of it is meant to be readable. */
.menu-id {
    margin: 0 0 var(--s1);
    font-weight: 600;
    color: var(--ink-strong);
    overflow-wrap: anywhere;
}

.menu-sub {
    margin: 0;
    color: var(--muted);
    font-size: var(--t-small);
}

.menu-sep {
    border: 0;
    border-top: 1px solid var(--line);
    margin: var(--s3) 0;
}

.menu-item {
    display: block;
    padding: var(--s2) 0;
    color: var(--ink);
    text-decoration: none;
}

.menu-item:hover {
    color: var(--action);
}

/* Sign out is not the page's action — it is the way out of it. Outlined, and full width so it
   reads as the foot of the menu rather than as one more line of it. */
.menu-action {
    margin-top: var(--s2);
    width: 100%;
    padding: var(--s2) var(--s4);
    font: inherit;
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    cursor: pointer;
}

.menu-action:hover {
    border-color: var(--ink);
}

/* --- Narrow screens ---------------------------------------------------- */

@media (max-width: 767.98px) {
    /*
       NOT STICKY HERE, in either realm.

       This is operator.css's rule generalised rather than a new one. It argued that a sticky band
       is worth 44px of a desktop and not worth a third of a phone, on a page that is mostly one
       tall text box — and the band costs far more than 44px here, because it wraps: eight portal
       destinations take three rows at 375px, and brand and account take one each. A fifth of the
       viewport permanently occupied by chrome is worse than a flick to the top of the page.

       SAAS-162 MADE IT TRUER RATHER THAN OBSOLETE. The eight destinations are no longer inside the
       header at this width — they are the row below it — but they are still on screen, and the
       current area's views are a band below that again. Direction B pays 252px of chrome once, and
       once is the whole point: sticking it would spend that on every viewport for the rest of the
       page.

       Sign-out is still reachable without scrolling on arrival at every page, which is what
       SAAS-116 asked for; what it is not, at this width, is reachable without scrolling *back*.
    */
    .site-header {
        position: static;
    }

    /* AND SO A LANDED ROW OWES NOTHING (SAAS-170). The offset above buys clearance from a sticky
       band; there is no sticky band here, so paying it would push the row a band's height further
       down the screen than it needs to be for no reason at all. The rule that spends it is written
       once and reads this token, so the phone case is a value rather than a second rule. */
    :root {
        --landing: 0px;
    }

    /* Match the page's gutter, so the wordmark sits over the page's own left edge rather than 8px
       right of it. */
    .masthead-inner {
        padding-inline: var(--s4);
        gap: var(--s3);
    }

    .site-main {
        padding-inline: var(--s4);
        padding-block: var(--s5) var(--s7);
    }

    /* The band is already two or three rows here; a fixed 16rem popover would hang off the right
       edge of a 375px viewport. It spans what is left of the row instead. */
    .menu-pop {
        width: min(18rem, calc(100vw - var(--s6)));
    }
}

/* --- Interaction basics ----------------------------------------------- */

/* One ring, everywhere. Never removed: a keyboard reader who cannot see focus cannot use the
   branch checkboxes at all. */
:focus-visible {
    outline: 2px solid var(--action);
    outline-offset: 2px;
    border-radius: var(--r-sm);
}

a {
    color: var(--action);
}

/* ----------------------------------------------------------------------
   THE ROW YOU CAME BACK TO (SAAS-170).

   A back crumb rebuilds the reader's list and ends in `#<row>`. The browser then does two things
   with that on its own, and both are the reason this slice needs no JavaScript at all: it scrolls
   the named element into view, and it matches `:target` against it so a stylesheet can mark it.
   Nothing on these Surfaces runs script — there is no circuit here — so a cure that had to scroll a
   window after the fact was never available.

   ONE SELECTOR RATHER THAN A LIST OF ELEMENTS. A row is a `tr` on six tables and an `li` on the
   medical-records chronology, and the next list to grow a crossing will be something else again.
   `[id^="r_"]` is RowAnchor's own prefix, so what is selected is exactly "an anchored row", present
   and future, rather than a list of tag names somebody has to remember to extend.

   THE OFFSET IS THE ROW'S OWN, not the frame's. `scroll-padding-top` on the scrolling element would
   have offset every fragment landing in the product from a slice about back crumbs — including the
   report previews and the day-end blocks, which scroll inside boxes of their own. Scoped to the
   rows this slice anchors, the change cannot reach anything it was not asked to.

   THE MARK IS THE WASH AND NOTHING ELSE. --teal-wash is already this product's "this is the one you
   are on" — it is the selected row a lit star sits on (see --star) — so a landed row wears the
   idiom the reader has already learnt rather than a new one. It is not a status and gets none of
   the ladder's hues, and it fades on its own the moment the reader clicks anything else, because
   `:target` stops matching when the fragment does.
   ---------------------------------------------------------------------- */
[id^="r_"]:target {
    scroll-margin-top: var(--landing);
}

/* On a table the wash goes on the cells, which is this sheet's rule for every row state — a
   `background` on a `tr` is painted under the cells rather than by them once the borders are
   collapsed. Specificity beats the hover wash beside it deliberately: where the pointer happens to
   be resting matters less than where the reader was sent. */
table tbody tr[id^="r_"]:target > td {
    background: var(--teal-wash);
}

/* The chronology's entries are list items with no box of their own. The spread shadow is what gives
   the wash its padding without giving the entry a size — a rule that added real padding would move
   the page under the reader at the exact moment they were trying to find their place on it. */
li[id^="r_"]:target {
    background: var(--teal-wash);
    border-radius: var(--r-sm);
    box-shadow: 0 0 0 var(--s2) var(--teal-wash);
}

button,
[type="submit"] {
    transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease;
}

/* Touch targets are the last section of this sheet, not this one. The 44px floor has to be able to
   beat the rule it is flooring, and at equal specificity that means coming last — see THE 44px
   FLOOR (SAAS-163) at the foot of the file. */

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: .001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .001ms !important;
    }
}

table.branches {
    border-collapse: collapse;
    width: 100%;
}

/* The row's height is the density ruling (SAAS-86): --row-y top and bottom against a 14px body
   gives the 42px row. Roomy (15.5px / 48px) and compact (12.5px / 36px) were both built and
   refused. It is written as a token rather than a value because the report table, the register,
   the ledger and the items table are all this same row, and density that has to be changed in
   four places is density nobody will change. */
table.branches th,
table.branches td {
    border-bottom: 1px solid var(--line);
    padding: var(--row-y) var(--s3) var(--row-y) 0;
    text-align: left;
    vertical-align: top;
}

table.branches th {
    font-size: var(--t-small);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
}

td .ok {
    color: var(--ok);
    font-weight: 600;
}

td .notice {
    color: var(--warn);
}

tr.unreached td {
    background: color-mix(in srgb, var(--warn) 6%, transparent);
}

code {
    font-family: var(--mono);
}

/* The three small centred forms: the sign-in, the change-password page SAAS-120 put behind
   the account menu, and the Accept page SAAS-179 put outside both fences. Named beside
   .portal-signin rather than given rules of its own — it is the
   same shape, and a second copy of a form's measurements is how two screens drift apart. */
.portal-signin,
.portal-account,
.portal-accept,
.portal-home {
    max-width: 24rem;
}

.portal-signin .field,
.portal-account .field,
.portal-accept .field {
    margin: 0 0 var(--s4);
}

.portal-signin label,
.portal-account label,
.portal-accept label {
    display: block;
    font-size: var(--t-small);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
    margin-bottom: var(--s1);
}

.portal-signin input,
.portal-account input,
.portal-accept input {
    width: 100%;
    box-sizing: border-box;
    padding: var(--s2);
    font: inherit;
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
}

/* The sign-in submit is the one action on its page, so it is the filled one. */
.portal-signin button,
.portal-account button,
.portal-accept button,
.portal-home button {
    padding: var(--s2) var(--s4);
    font: inherit;
    font-weight: 600;
    border: 1px solid transparent;
    border-radius: var(--r-sm);
    background: var(--action);
    color: var(--on-action);
    cursor: pointer;
}

.portal-signin button:hover,
.portal-account button:hover,
.portal-accept button:hover,
.portal-home button:hover {
    background: var(--action-hover);
}

/* The Accept page's one refusal (SAAS-179) wears the sign-in refusal's box. It is a class of its
   own rather than the same one because the page draws both — this for a link that no longer
   opens anything, .signin-error for a password the form itself refused — and a reader looking at
   the markup should be able to tell which happened. */
.signin-error,
.accept-refusal {
    border: 1px solid color-mix(in srgb, var(--bad) 40%, transparent);
    background: color-mix(in srgb, var(--bad) 8%, transparent);
    color: var(--bad);
    border-radius: var(--r-sm);
    padding: var(--s2) var(--s3);
    max-width: 60ch;
}

dl.portal-identity {
    margin: 0 0 var(--s5);
}

dl.portal-identity dt {
    font-size: var(--t-small);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
    margin-top: var(--s2);
}

dl.portal-identity dd {
    margin: 0;
}

/* --- The dashboard (SAAS-40, typeset in SAAS-51) ------------------------ */

/* No cap. It used to be 62rem, which was left behind when the shell stopped being centred
   (SAAS-49): the header's clinic group sits against the right edge of the viewport, so a 62rem
   page beneath it left that caption stranded far out past the table it belongs to. The page now
   reaches the same edge the chrome does. Prose is still measured — `.lead`, the legend and a
   branch's stated message each carry their own limit, because a sentence 1900px wide is
   unreadable in a way a table is not. */
.portal-dashboard {
    max-width: 100%;
}

.muted {
    color: var(--muted);
}

.small {
    font-size: var(--t-body);
}

/* Row rhythm. These rows run two and three lines tall — a stamp over its age, a stack of sources
   beside it — so the padding has to be set against the row's *height* rather than against a line
   of text. Below about .8rem the gap between one branch's second line and the next branch's first
   is smaller than the gap inside the row, and the eight branches read as one block of text. */
table.freshness th,
table.freshness td {
    padding-block: var(--s3);
    padding-inline-end: var(--s6);
}

table.freshness tbody tr:hover td {
    background: var(--row-hover);
}

/* Same rule as the report results, for the same reason: four columns of stamps and source names
   have a min-content width no phone has, and a table that pushes the whole page sideways is worse
   than one that scrolls inside its own box. Measured at 375px — the page's own scrollWidth stays
   at the viewport. */
.freshness-scroll {
    overflow-x: auto;
}

table.freshness td.branch-name {
    font-weight: 600;
    color: var(--ink-strong);
}

/* Figures line up down the column. This is the reason the design system self-hosts Inter — eight
   stamps of the form `2026-08-08 13:20` are a column of digits, and proportional figures make a
   column of digits ragged for no reason. */
table.freshness td.figures {
    font-variant-numeric: tabular-nums;
}

/* Nothing wraps on a wide screen: a branch name broken over two lines while empty space sits to
   the right of the last column is just untidy. Below the breakpoint the opposite is true — there
   the row needs every chance to fit, so these are scoped rather than global. */
@media (min-width: 768px) {
    table.freshness td.branch-name,
    table.freshness td.figures {
        white-space: nowrap;
    }
}

table.freshness td.figures .stamp {
    color: var(--ink-strong);
}

/* A branch that could not be measured. Neutral, not amber: the row is a stated condition, not a
   fault, and the previous wash made "Not connected yet" look like a system in trouble. */
table.freshness tr.unreached td {
    background: color-mix(in srgb, var(--muted) 5%, transparent);
}

table.freshness tbody tr.unreached:hover td {
    background: color-mix(in srgb, var(--muted) 9%, transparent);
}

/* The bucket pill. Three weights, not five: an hourly sync is either keeping up, drifting, or
   broken, and colouring every rung differently would imply a precision the ladder has not.
   `FreshnessWeights` is the C# half of this rule, and both the cells and the legend read from it. */
.bucket {
    display: inline-block;
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: var(--s1) var(--s2);
    font-size: var(--t-body);
    white-space: nowrap;
}

.bucket.current {
    color: var(--ok);
    border-color: color-mix(in srgb, var(--ok) 40%, transparent);
    background: color-mix(in srgb, var(--ok) 8%, transparent);
}

.bucket.lagging {
    color: var(--warn);
    border-color: color-mix(in srgb, var(--warn) 40%, transparent);
    background: color-mix(in srgb, var(--warn) 8%, transparent);
}

.bucket.stale {
    color: var(--bad);
    border-color: color-mix(in srgb, var(--bad) 40%, transparent);
    background: color-mix(in srgb, var(--bad) 10%, transparent);
}

/* The by-source list. It reads as a footnote to its row, not as a second table: it used to be
   capped at 20rem and justified apart, which pushed every age to the same right-hand edge and
   built a second numeric column that carried as much visual weight as the branch's own status.
   The ages now sit against their source, quiet, and only the governing one is emphasised.

   The list is why the indicator exists — Bandung's payments are current while its billing froze
   ten months ago — so it is subordinated, never dropped. It stays one stacked line per source:
   three sources is a short list, and flowing them across the row was measured doing nothing at
   any width the product is read at while making the row's shape depend on table-layout
   arithmetic. */
ul.sources {
    list-style: none;
    margin: 0;
    padding: 0;
    font-size: var(--t-small);
    line-height: 1.65;
    color: var(--muted);
}

ul.sources li {
    display: flex;
    gap: var(--s2);
    align-items: baseline;
}

/* The table that set the branch's status — the one an owner is being asked to look at. Stated in
   words in the "Last received" cell as well; this is the same fact where the sources are listed. */
ul.sources li.governing {
    color: var(--ink);
    font-weight: 600;
}

ul.sources .age::before {
    content: "·";
    margin-inline-end: var(--s2);
    color: var(--border-strong);
}

/* Never bold, even on the governing row: the age is the supporting detail, and the source name is
   what an owner is scanning for. */
ul.sources .age {
    font-weight: 400;
    color: var(--muted);
}

p.stated {
    margin: 0 0 var(--s1);
    max-width: 44ch;
    color: var(--muted);
    font-size: var(--t-body);
}

/* A stated condition wears the pill's shape and none of the ladder's colour. "Not connected yet"
   is not a rung and not a fault — it is the absence of a measurement — and painting it amber, as
   the old `.notice` did, made an un-onboarded branch look like a failing one. Achromatic also
   keeps the palette rule intact: colour on this page means the ladder, and nothing else.

   Scoped to the dashboard. `.notice` on the ops fleet table carries a whole Draining sentence,
   which a nowrap pill would ruin. */
.portal-dashboard .notice {
    display: inline-block;
    border: 1px solid var(--border-strong);
    border-radius: 999px;
    padding: var(--s1) var(--s2);
    font-size: var(--t-body);
    white-space: nowrap;
    color: var(--muted);
    background: transparent;
}

/* The key. Three pills, because three colours exist — the five rung captions are listed beside the
   colour each one wears, which is the fact the old five-pill row could not state without drawing
   two pairs of identical pills. */
.legend {
    margin-top: var(--s6);
    padding-top: var(--s4);
    border-top: 1px solid var(--line);
    max-width: var(--measure);
}

.legend h2 {
    font-size: var(--t-small);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
    margin: 0 0 var(--s3);
}

.legend dl {
    display: grid;
    grid-template-columns: max-content 1fr;
    align-items: baseline;
    gap: var(--s2) var(--s4);
    margin: 0;
}

.legend dt,
.legend dd {
    margin: 0;
}

.legend dd {
    color: var(--muted);
    font-size: var(--t-body);
}

.portal-footer {
    display: flex;
    align-items: center;
    gap: var(--s4);
    margin-top: var(--s6);
    padding-top: var(--s4);
    border-top: 1px solid var(--line);
}

/* Sign out is not the page's action — it is the way out of it. Outlined. */
.portal-footer button {
    padding: var(--s2) var(--s4);
    font: inherit;
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    cursor: pointer;
}

.portal-footer button:hover {
    border-color: var(--ink);
}

/* --- The report page (SAAS-43) ----------------------------------------- */

/* No cap. This page carries the widest thing in the product — the commission report runs to ten
   columns — and every pixel withheld here is a column pushed off the right edge while empty
   viewport sits beside it. */
.portal-reports {
    max-width: 100%;
}

/* The "Run a report →" link that used to sit here is gone (SAAS-54) — the sidebar is the way
   between Surfaces now, and a second route to Reports on one page is a menu that disagrees with
   itself about where navigation lives. */

.report-form {
    display: grid;
    grid-template-columns: minmax(18rem, 2fr) minmax(14rem, 1fr) minmax(14rem, 1fr);
    gap: var(--s5) var(--s6);
    align-items: start;
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    background: var(--surface);
    box-shadow: var(--e1);
    padding: var(--s4) var(--s5);
    margin-bottom: var(--s5);
}

.report-form .field > label,
.report-form .custom-range label {
    display: block;
    font-size: var(--t-small);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
    margin-bottom: var(--s1);
}

.report-form select,
.report-form input[type="search"],
.report-form input[type="date"] {
    width: 100%;
    box-sizing: border-box;
    padding: var(--s2);
    font: inherit;
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
}

/* The open dropdown is painted by the platform, not the page: a transparent select
   leaves the list on the OS default white while `color: inherit` hands it the page's
   light-on-dark text, so in dark mode every option but the highlighted one renders
   white on white. Pin the list to the system field colours, which follow
   `color-scheme` in both themes. The closed control stays transparent. */
.report-form select option {
    background: Field;
    color: FieldText;
}

/* Secondary: Select all / Clear. Outlined, because they sit beside the one filled control on
   the page and two filled buttons would give a reader no way to tell which one runs the report. */
.report-form button {
    padding: var(--s2) var(--s4);
    font: inherit;
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    cursor: pointer;
}

.report-form button:hover:not(:disabled) {
    border-color: var(--ink);
}

/* Run report — the page's one action, and the only filled thing on it. */
.report-form button.primary {
    font-weight: 600;
    padding: var(--s2) var(--s5);
    border-color: transparent;
    background: var(--action);
    color: var(--on-action);
}

.report-form button.primary:hover:not(:disabled) {
    background: var(--action-hover);
    border-color: transparent;
}

.report-form button:disabled {
    opacity: .5;
    cursor: default;
}

/* --- The report list, beside the form (SAAS-133, ADR 0024 ruling 6) -----

   The drop-down is gone. Darius ruled this shape from a rendered comparison against a grouped
   drop-down with a star beside it: forty reports and favourites are both about FINDING, and a list
   you can see and search beats one you must open.

   ~300px is the ruled comparison's own figure, carried over rather than re-derived. A name too long
   for it WRAPS rather than being clipped — nothing here sets `white-space`, and `.nm` is a flex row
   with `min-width: 0` so the name may shrink and fold — which matters because the standard set runs
   to names like "Appointment Attendance and No-shows" and a clipped report name is a report a reader
   cannot tell from its neighbour. Under 900px the column becomes a row above the form: two
   300px-ish columns do not fit a phone, and a list squeezed to 40% of a 375px screen is worse than
   one that simply comes first. */
.report-split {
    display: grid;
    grid-template-columns: 300px minmax(0, 1fr);
    gap: var(--s5);
    align-items: start;
}

@media (max-width: 899.98px) {
    .report-split {
        grid-template-columns: minmax(0, 1fr);
    }
}

.report-list {
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    background: var(--surface);

    /* Bounded and scrolled rather than as long as the library: the form beside it must stay on
       screen while a reader hunts, which is the whole advantage of the split. */
    max-height: 32rem;
    overflow: auto;
}

/* Inert while a run is in flight, exactly as the drop-down was disabled mid-run: swapping the
   report under a spinner that names the old one is a screen disagreeing with itself. The page
   refuses the click too — this only says so. */
.report-list.busy {
    opacity: .6;
    pointer-events: none;
}

/* The search box's label. Present for a screen reader and off-screen for everyone else: the
   placeholder is the visible label here, and a placeholder is not a label. */
.report-list .find-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.report-list .find {
    position: sticky;
    top: 0;
    z-index: 2;
    width: 100%;
    box-sizing: border-box;
    padding: var(--s2) var(--s3);
    border: 0;
    border-bottom: 1px solid var(--border);
    border-radius: var(--r-md) var(--r-md) 0 0;
    background: var(--surface);
    color: var(--ink);
    font: inherit;
}

/* One Report Category, or the pinned group. Sticky under the search box so a reader scrolling
   forty rows always knows which shelf they are on. */
.report-list .group {
    position: sticky;
    top: 2.35rem;
    z-index: 1;
    margin: 0;
    padding: var(--s3) var(--s3) var(--s1);
    background: var(--surface);
    font-size: var(--t-micro);
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--muted);
}

/* ★ Favourites. The heading wears the star's own colour because the group IS the star's
   consequence — 5.43:1 on --surface, measured. */
.report-list .group.fav {
    color: var(--star);
}

.report-list ul {
    list-style: none;
    margin: 0 0 var(--s2);
    padding: 0;
}

.report-list li {
    display: flex;
    align-items: center;
    gap: var(--s1);
    padding-right: var(--s2);
}

.report-list li:hover {
    background: var(--row-hover);
}

/* The row itself is a link to this page carrying `?report=`, so the choice survives a reader with
   no script and the address can be bookmarked. It is laid out as a row rather than as a link
   because that is what it behaves like. */
.report-list li .nm {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: baseline;
    gap: var(--s2);
    padding: var(--s2) 0 var(--s2) var(--s3);
    color: var(--ink);
    text-decoration: none;
}

.report-list li .nm:hover {
    text-decoration: none;
}

.report-list li .nm .n {
    min-width: 0;
}

/* The chosen row. --teal-deep on --teal-wash measures 6.35:1 light and 7.98:1 dark. */
.report-list li.sel {
    background: var(--teal-wash);
}

.report-list li.sel .nm {
    color: var(--teal-deep);
    font-weight: 600;
}

/* "· custom", visible BEFORE the report is picked — the open list's one advantage over the
   drop-down that a drop-down could not have had, because a native option list will not promise to
   honour a colour. 6.62:1 on --surface, 5.87:1 on a selected row. */
.report-list li .c {
    flex: none;
    font-size: var(--t-micro);
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--scope-custom);
}

/* The star.

   UNLIT IS --muted, NOT --border-strong. The rendered comparison used the border hue, which
   measures 1.47:1 against --surface — an interactive control's only state indicator, effectively
   invisible. --muted is 4.97:1 on --surface and 4.41:1 on a selected row: above the 3:1 a non-text
   state indicator owes, and the correction is the one thing on this control that is not the mock's.
   Lit is --star, measured with the token. */
.report-list li .st {
    flex: none;
    border: 0;
    border-radius: var(--r-sm);
    background: none;
    color: var(--muted);
    cursor: pointer;
    font-size: var(--t-lead);
    line-height: 1;
    padding: var(--s1) var(--s2);
}

.report-list li .st.on {
    color: var(--star);
}

.report-list li .st:hover {
    color: var(--star);
}

.report-list .empty {
    margin: 0;
    padding: var(--s4) var(--s3);
}

/* The form's own column. Two fields wide now that the picker has left it — scoped to this page,
   because .report-form is shared verbatim with the Day End Surface, which still has three. */
.portal-reports .report-form {
    grid-template-columns: minmax(12rem, 1fr) minmax(12rem, 1fr);
    margin-bottom: 0;
}

/* The chosen report, heading the form it belongs to. */
.report-side .chosen {
    margin-bottom: var(--s4);
}

.report-side .chosen .t {
    margin: 0 0 var(--s1);
    font-size: var(--t-h2);
    color: var(--ink-strong);
}

.report-side .chosen .description {
    margin: 0;
    color: var(--muted);
    max-width: 62ch;
}

.report-pick .description {
    margin: var(--s2) 0 0;
    color: var(--muted);
    max-width: 52ch;
}

/* Standard or this clinic's own — the difference between "a report" and "our report". */
.scope {
    display: inline-block;
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 0 var(--s2);
    margin-right: var(--s2);
    font-size: var(--t-small);
    text-transform: uppercase;
    letter-spacing: .04em;
    white-space: nowrap;
}

.scope.custom {
    color: var(--scope-custom);
    border-color: color-mix(in srgb, var(--scope-custom) 40%, transparent);
    background: color-mix(in srgb, var(--scope-custom) 8%, transparent);
}

.custom-range {
    display: grid;
    grid-template-columns: 3rem 1fr;
    align-items: center;
    gap: var(--s1) var(--s2);
    margin-top: var(--s2);
}

.custom-range label {
    margin-bottom: 0;
}

.resolved {
    margin: var(--s2) 0 0;
    font-size: var(--t-body);
}

/* Wrap the row, never the labels inside it.
   Inter is wider than the system fonts this row was laid out against, so after SAAS-49 the
   three items no longer fitted the Branches column and flex compensated by squeezing the
   buttons until "Select all" broke across two lines. A button whose own label wraps reads as
   broken; a row that moves its last item to the next line reads as a layout. */
.select-all {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s2);
    margin: 0 0 var(--s2);
}

.select-all button {
    white-space: nowrap;
    flex: 0 0 auto;
}

ul.branches-select {
    list-style: none;
    margin: 0;
    padding: 0;
    /* Ten branches fit without scrolling — the pilot's fleet is eight, and a list that clips at
       seven reads as a bug. Larger tenants scroll. */
    max-height: 17rem;
    overflow-y: auto;
}

ul.branches-select label {
    display: flex;
    align-items: center;
    gap: var(--s2);
    padding: var(--s1) 0;
    text-transform: none;
    letter-spacing: normal;
    font-size: inherit;
    color: inherit;
    margin: 0;
}

ul.branches-select label.moved {
    color: var(--muted);
}

.run {
    grid-column: 1 / -1;
    display: flex;
    align-items: center;
    gap: var(--s4);
    border-top: 1px solid var(--line);
    padding-top: var(--s4);
}

/* The running indicator. DAOv4 blocked the whole tab; the elapsed counter ticking beside this
   spinner is the page demonstrating that it does not. */
.running {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
}

.spinner {
    width: .9rem;
    height: .9rem;
    border: 2px solid var(--line);
    border-top-color: currentColor;
    border-radius: 50%;
    animation: spin .7s linear infinite;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

/* The report's one Filter Variable (SAAS-147). It sits in the form's grid like the period and the
   branches, because it is the third thing a reader chooses about this run — and unlike either of
   them it may be left alone, which the hint under it says rather than the layout. */
.filter-pick .muted {
    display: block;
    margin-top: var(--s1);
}

/* THE LOCK, where a doctor-restricted report's box would be (SAAS-148). Deliberately NOT styled to
   look like a disabled input: it is a sentence, because there was never a field here for this
   reader. It occupies the same slot in the form's rhythm as .filter-pick so the run form does not
   reflow depending on who is reading it. */
.filter-locked .locked {
    margin: var(--s1) 0 0;
    color: var(--ink);
}

.filter-locked .locked strong {
    /* The one word that answers "narrowed to what?", carried in the chrome's own teal so it reads
       as a stated fact about this run rather than as the placeholder text of an empty box. */
    color: var(--teal-deep);
}

.filter-locked .muted {
    display: block;
    margin-top: var(--s1);
}

/* What the table below was narrowed to, said over the table rather than only in the file. A
   reader who scrolled back to a result must not mistake one doctor's rows for the clinic's. */
.ran-header p.ran-filter {
    /* Pulled up under the period line rather than spaced from it: the two are one block of
       metadata about the same run, and `.ran-header p` has already put a gap below that one. */
    margin: calc(var(--s4) * -1) 0 var(--s4);
    color: var(--muted);
}

.ran-header p.ran-filter strong {
    color: var(--ink);
}

/* The report's own name on the results page: a section heading, one step under the page title
   and one above the per-Branch blocks. */
.ran-header h2 {
    font-size: var(--t-h2);
    letter-spacing: -.014em;
    margin: 0 0 var(--s1);
}

.ran-header p {
    margin: 0 0 var(--s4);
}

/* The Excel export (SAAS-45, and the Day End's in SAAS-96) — written from the result already on
   screen, never a second run.

   One rule for both surfaces rather than a copy under .doc-masthead: the two buttons do the same
   thing and a reader who has used one should recognise the other. A second block would be a second
   place "what an export button looks like" lives, free to drift the way the two stylesheets this
   product deliberately does not have would have. */
p.download {
    display: flex;
    align-items: center;
    gap: var(--s2);
}

p.download button {
    padding: var(--s2) var(--s4);
    font: inherit;
    font-weight: 600;
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    cursor: pointer;
}

p.download button:hover:not(:disabled) {
    border-color: var(--ink);
}

p.download button:disabled {
    opacity: .5;
    cursor: default;
}

/* The Day End's sits under the masthead rather than beside a heading, so it needs the gap the
   .ran-header paragraph rule was giving the other one. */
.doc-masthead p.download {
    margin: var(--s3) 0 0;
}

/* --- The report result (typeset in SAAS-50) ----------------------------- */

/* This is the product's real surface — dense, numeric, money — and until now it had had no
   typographic attention at all. The rules below are one idea in six places: a financial table is
   read *down* its columns, so the page has to hold a column together (one alignment, tabular
   figures, headings that stay put) and then say which block a reader is in (a branch name that
   outranks the column headers, and — since SAAS-55 — a summary band above them all that outranks
   every branch).

   All of it is achromatic. The design system spends colour on exactly two things — the one primary
   action, and status — and "this row is the total" is neither, so weight, a rule and a wash of ink
   carry it, exactly as the sidebar marks the page you are on. */

.report-results .branch-block {
    margin: 0 0 var(--s6);
}

/* The branch name is a heading, not a caption. It was .95rem — *smaller* than the page's own body
   text — so nine blocks repeated at one weight gave a reader scanning for one branch nothing to
   land on. At 1.05rem/650 over column headers of .8rem uppercase muted, the hierarchy inside a
   block is unambiguous: the branch owns the block, the columns merely describe the table. */
.report-results .branch-block h3 {
    font-size: var(--t-h3);
    font-weight: 650;
    letter-spacing: -.01em;
    color: var(--ink-strong);
    margin: 0 0 var(--s2);
    padding-bottom: var(--s1);
    border-bottom: 1px solid var(--border-strong);
}

/* Left as SAAS-43 set it, deliberately: a Branch that could not be read is a *status*, which is
   one of the two things this palette spends colour on. It is worth a ruling all the same — SAAS-51
   made the dashboard's stated conditions achromatic, on the grounds that "no report credential" is
   the absence of a measurement rather than a fault, and these are the very same conditions
   (NotMinted, Superseded, ExecutionFailed) wearing amber on this page. */
.report-results .branch-block.refused h3 {
    color: var(--warn);
}

/* Tabular figures on the whole table rather than on the numeric columns alone. `td.num` is the
   obvious scope and it is too narrow: a date column is a column of digits that MySQL hands back as
   a DateTime, so it is never "numeric" and would keep proportional figures while the money beside
   it lined up. This is what self-hosting Inter was for. */
.report-results table.report {
    font-size: var(--t-body);
    font-variant-numeric: tabular-nums;
}

/* A column carries one alignment down its whole length, heading included. The heading half of this
   rule was written in SAAS-43 and never once matched: the component rendered no class on any `th`,
   so every money column in the product had a left-aligned heading sitting over right-aligned
   figures. Which columns these are is decided once per result, in ReportValueText.NumericColumns,
   so one null cannot flip a cell out of its column.

   The leading gutter is for the money-beside-a-count case: two right-aligned columns otherwise
   leave only the trailing .75rem between the last digit of one and the first of the next, and a
   count of 502 reads as part of the total it sits against. */
.report-results td.num,
.report-results th.num {
    text-align: right;
    padding-inline-start: var(--s5);
}

/* The column meanings survive a long branch. Two things make this work, and both are load-bearing.

   The scroll box moved *into* the block. A sticky element is positioned against its nearest scroll
   box, and `overflow-x: auto` computes `overflow-y` to `auto` too — so the old stack-wide
   `.report-results { overflow-x: auto }` was a scroll box of exactly content height, which never
   scrolls vertically and against which a sticky heading therefore never moves. Measured before the
   move, with the sticky rules already in place: scrolling 900px moved the heading 900px, i.e. it
   did nothing at all.

   And the box is capped, so that it scrolls in the first place. A block shorter than the cap is
   untouched — the collections report's five rows a branch never becomes a scroll pane — while the
   commission Custom's 1,206-row Puri block gets a scrollport of its own and keeps its headings.

   A report is still as wide as its columns, and the commission Custom is 24 of them, so this box
   goes on carrying the horizontal scroll the stack used to: a table that pushes the whole page
   sideways is worse than one that scrolls inside itself, which is what .site-main's `min-width: 0`
   makes possible. */
.report-results .block-scroll {
    overflow: auto;
}

@media (min-width: 768px) {
    /* Wide screens only. Below the breakpoint the box still scrolls sideways but is never capped,
       so vertical scrolling stays the page's — a thumb trapped in a 70vh box that also scrolls
       horizontally is a worse bargain than losing the headings, and on one column the headings are
       the shorter journey back anyway. */
    .report-results .block-scroll {
        max-height: 70vh;
    }
}

.report-results thead th {
    position: sticky;
    top: 0;
    z-index: 1;

    /* Opaque, or the rows read straight through the headings as they hold. */
    background: var(--page);

    /* `border-collapse: collapse` gives the rule to the table rather than to the cell, so a sticky
       heading leaves its own underline behind on the row it detached from. An inset shadow is
       painted by the cell and travels with it. */
    border-bottom: 0;
    box-shadow: inset 0 -1px 0 var(--line);
}

/* A document, not an app grid: the row under the pointer lifts by three and a half percent of ink,
   and nothing else. No zebra — that would draw sixty horizontal bands the data does not have.
   Scoped to a Branch's own rows, because hovering the only row of the totals table says nothing. */
.report-results .branch-block.ran tbody tr:hover td {
    background: var(--row-hover);
}

/* --- The summary band (SAAS-55) ---------------------------------------- */

/* The band answers the question the page could not: what did each branch take. One table above the
   blocks — a row per branch over the summable columns only, with the fleet total as its last row.
   The separate "All branches" block is gone rather than sitting beside it, because two places
   stating one fleet total is the redundancy the band exists to remove.

   Its treatment is that block's, moved: a wash of ink, a heading a size above the branches' own,
   and a full-strength rule above the fleet figure. Achromatic throughout, for the reason at the
   top of this section — "this is a subtotal" is neither the page's one primary action nor a
   status, so weight, a rule and a wash carry it.

   The wash is 3% where the totals block carried 4%. It runs behind nine rows here rather than one,
   and it sits above the detail rather than under it: the same value that read as emphasis on a
   one-row block reads as a panel the eye stops at when it is nine rows tall at the top of the
   page. 3% keeps it the same document, emphasised. */
.report-results .branch-block.summary {
    margin-bottom: var(--s7);
    padding: var(--s4) var(--s5) var(--s4);
    border-radius: var(--r-lg);
    background: color-mix(in srgb, var(--ink) 3%, transparent);
}

.report-results .branch-block.summary h3 {
    font-size: var(--t-h3);
    border-bottom-color: var(--ink);
}

/* The wash sits behind the headings too, so their opaque background has to be the wash composited
   against the page rather than the page itself — otherwise a heading holding inside the band paints
   a lighter rectangle over the block it belongs to.

   And the headings are ink here rather than muted, which is a contrast fix before it is a design
   choice: SAAS-50 measured muted grey at 4.68:1 on the page and 4.32:1 with a wash behind it, and
   12.8px text owes 4.5:1. Ink clears it with room to spare — and it is the right answer anyway,
   since this is the block meant to read strongest on the page. */
.report-results .branch-block.summary thead th {
    background: color-mix(in srgb, var(--ink) 3%, var(--page));
    color: var(--ink);
}

/* The fleet total is not one of the branches above it — it is what they add up to — so a
   full-strength rule separates it, which is the idiom a financial statement uses above a total. */
.report-results .branch-block.summary tr.totals-row td {
    border-top: 2px solid var(--ink);
}

/* A branch that could not be read carries its stated condition where its figures would be — never
   a blank, and never a zero, which would claim it collected nothing. Ink rather than muted, for the
   contrast reason above, and normal weight so a sentence is never mistaken for a figure. It wraps
   rather than truncating: an execution failure carries MySQL's own words, and clipping them to keep
   the band tidy would hide the only thing that says what went wrong. */
.report-results .branch-block.summary td.stated {
    color: var(--ink);
    font-weight: 400;
    font-size: var(--t-body);
}

/* The band is read *across* a row — this branch, its transactions, its money — where a block is
   read down a column. So the row under the pointer lifts, exactly as a branch's own rows do. Not
   the fleet row: it is the one row with nothing to compare it against. */
.report-results .branch-block.summary tbody tr:not(.totals-row):hover td {
    background: var(--row-hover);
}

/* The withheld fleet total, stated inside the band where its row would have been. */
.report-results .branch-block.summary p.no-totals {
    margin: var(--s4) 0 0;
}

/* The figure an owner came for is not the same size as the rows above it. */
.report-results tr.totals-row td {
    font-size: var(--t-lead);
    font-weight: 650;
    color: var(--ink-strong);
    border-bottom: 0;
}

/* The report form is three columns of 18rem/14rem/14rem — 50rem of minimums, which no phone has.
   One column under the breakpoint, for the same reason the nav becomes a drawer there. */
@media (max-width: 767.98px) {
    .report-form {
        grid-template-columns: 1fr;
        gap: var(--s4);
        padding: var(--s4);
    }

    /* And the report page's own two, which SAAS-133 scoped more specifically than the rule above
       so that the Day End Surface could keep three. Specificity does not care that this one is in
       a media query, so the narrow case has to be scoped the same way or the phone keeps two
       columns of 12rem on a 375px screen. */
    .portal-reports .report-form {
        grid-template-columns: minmax(0, 1fr);
    }

    .run {
        flex-wrap: wrap;
    }
}

/* The three boxed statements: a whole run that refused, a form that could not run, and the
   all-branches total the Runner withheld. Each is a sentence a reader has to actually read.

   The ink is set here rather than inherited, and that is an accessibility fix carried in from
   SAAS-50's measuring rather than something that ticket asked for. These boxes take their colour
   from `p.stated`, which is `--muted` — right on the page, but these three put an amber wash behind
   it, and 13.6px muted grey on that wash measures 4.23:1 in the light theme, under the 4.5:1 that
   size owes. In ink on the same wash it is 15.11:1. The wash and the border still say which kind of
   statement it is; only the sentence itself got darker. */
p.run-refused,
p.run-problem,
p.no-totals,
p.stale-notice {
    border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent);
    background: color-mix(in srgb, var(--warn) 8%, transparent);
    color: var(--ink);
    border-radius: var(--r-sm);
    padding: var(--s2) var(--s3);
    max-width: 70ch;
}

/* The per-Surface freshness notice (SAAS-76). It joins the three boxed statements above rather
   than inventing a fourth look, because it is the same kind of sentence they are: something the
   product will not do, said in words, in the block it applies to. Inheriting their ink fix matters
   here too — the same 4.5:1 argument, the same wash.

   Sits inside a branch block and above its table, so it needs the gap the boxed three do not. */
p.stale-notice {
    margin: 0 0 var(--s4);
}

/* "Not measured yet" is not a warning. A Surface with no probe is an absence, and painting it
   amber would make Daclaria's own gap look like the clinic's — the same argument the dashboard's
   achromatic `.notice` pill already makes one screen away. */
p.stale-notice.unmeasured {
    border-color: var(--border-strong);
    background: none;
    color: var(--muted);
}

/* --- "Copy synced" (SAAS-212, ADR 0031 ruling 2) ------------------------ */

/* THE LINE IS QUIET BY DESIGN, and the ruling is the reason: "never a badge". It is on every
   Surface on every load, so it has to read as a caption under the heading rather than as an alert —
   a standing fact competing with the page's own content is a fact readers stop seeing by the third
   page, which would cost it exactly the job it was added for.

   Small, muted, and directly under the h1 it qualifies. No border, no wash, no pill: the boxed
   amber above is for sentences about something being WRONG, and most of the time this one is not. */
p.copy-synced {
    margin: calc(var(--s2) * -1) 0 var(--s4);
    font-size: var(--t-small);
    color: var(--muted);
}

/* The one state where it does raise its voice — the copy is behind, or its runs are failing. Still
   not a badge: the same line, in the ink the rest of the page warns in, so it changes weight
   without changing shape or moving anything under it. */
p.copy-synced.wants-attention {
    color: var(--ink);
    font-weight: 600;
}

/* The dashboard's sibling column (ruling 4). It sits beside "Data received" and must not out-shout
   the pill next to it, so it takes the figures column's own restraint. */
td.copy-synced-cell {
    color: var(--muted);
    white-space: nowrap;
}

td.copy-synced-cell.wants-attention {
    color: var(--ink);
    font-weight: 600;
}

/* --- The Patients Surface and the shared pager (SAAS-68) ---------------- */

/* No cap, for the reason the report page has none: this is a six-column table read across, and
   capping it is what put the sidebar in the middle of the screen. */
.portal-patients {
    max-width: none;
}

/* The branch picker and the search (SAAS-69). Nine controls now rather than one, so it is a grid
   that reflows rather than a row: `auto-fit` with a 14rem floor gives four columns on a desktop,
   two on a tablet and one on a phone with nothing to declare per breakpoint. It still wraps rather
   than pushing the page sideways, which is the rule that mattered when it was one control. */
.register-form {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    align-items: end;
    gap: var(--s3) var(--s4);
    margin: 0 0 var(--s5);
    padding: var(--s4) var(--s5);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    background: var(--surface);
    box-shadow: var(--e1);
}

.register-form .field {
    display: flex;
    flex-direction: column;
    gap: var(--s1);
    min-width: 0;
}

/* A pair of bounds is one control with two ends, so it takes two cells where there are two to
   take and folds into one where there are not. */
.register-form .field-range {
    grid-column: span 2;
}

@media (max-width: 719px) {
    .register-form .field-range {
        grid-column: span 1;
    }
}

.register-form .range {
    display: flex;
    align-items: center;
    gap: var(--s2);
}

.register-form .range input {
    flex: 1 1 0;
    min-width: 0;
}

.register-form .range-to {
    flex: 0 0 auto;
    font-size: var(--t-small);
    color: var(--muted);
}

/* Said once, under the control it is about. The wrap is the least guessable thing on this page:
   without it an Owner asking for 25 Dec to 5 Jan cannot tell whether they will get twelve days or
   nothing at all. */
.register-form .hint {
    margin: var(--s1) 0 0;
    font-size: var(--t-small);
    line-height: 1.45;
    color: var(--muted);
}

/* The buttons sit on their own row under the grid, so "Search" is never stranded in a cell beside
   a date box at one width and under it at the next. */
.register-form .actions {
    grid-column: 1 / -1;
    display: flex;
    align-items: center;
    gap: var(--s4);
    margin-top: var(--s1);
}

/* A link, not a second button. Clearing a search is a way back rather than an action of its own,
   and two buttons side by side would ask a reader which one they meant. */
.register-form .clear {
    font-size: var(--t-body);
    color: var(--muted);
}

.register-form .clear:hover {
    color: var(--ink);
}

.register-form input[type="search"],
.register-form input[type="date"] {
    width: 100%;
    min-width: 0;
    padding: var(--s2);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
}

/* What the register was narrowed to, read back. Muted, because it describes the table rather than
   competing with it — the same weight the pager's own count carries. */
.portal-patients .narrowed {
    margin: 0 0 var(--s3);
    font-size: var(--t-body);
    line-height: 1.5;
    color: var(--muted);
}

.register-form label {
    font-size: var(--t-small);
    font-weight: 600;
    letter-spacing: .01em;
    color: var(--muted);
}

.register-form select {
    width: 100%;
    padding: var(--s2);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
}

/* The open dropdown is painted by the platform, not the page — the report form's own rule. */
.register-form select option {
    background: var(--surface);
    color: var(--ink);
}

/* Outlined, not filled. Showing a register is not a page's one primary action; it is how a reader
   gets to the thing the page is for, and a filled button here would compete with nothing. */
.register-form button {
    padding: var(--s2) var(--s4);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    cursor: pointer;
}

.register-form button:hover {
    background: var(--row-hover);
}

/* Whose register this is. A heading rather than a caption — the same call SAAS-50 made for the
   report blocks, where the branch name had been smaller than the page's own body text. */
.portal-patients h2.branch-name {
    margin: 0 0 var(--s2);
    font-size: var(--t-h3);
    font-weight: 650;
    color: var(--ink-strong);
}

/* Six columns of names, dates and phone numbers have a min-content width no phone has. The table
   scrolls inside its own box so the page's own scrollWidth stays at the viewport — the rule the
   dashboard and the report results already keep, made possible by .site-main's min-width: 0. */
.register-scroll {
    overflow: auto;
}

/* And the box is capped, which is what makes the sticky headings below work at all — SAAS-50's
   finding, reproduced here before it was fixed. A sticky element is positioned against its nearest
   scroll box, and `overflow-x: auto` computes `overflow-y` to `auto` as well, so an uncapped box is
   a scroll box of exactly content height: it never scrolls vertically, and nothing can ever stick
   against it. Measured with the sticky rules already in place and no cap — scrolling the page 400px
   moved the heading 400px, which is to say it did nothing at all.

   Wide screens only, as there: below the breakpoint a thumb trapped in a box that also scrolls
   sideways is a worse bargain than losing the headings, and on one column the headings are the
   shorter journey back anyway. */
@media (min-width: 768px) {
    .register-scroll {
        max-height: 70vh;
    }
}

/* Tabular figures on the whole table, not on the numeric column alone: a birthday and a
   registration date are columns of digits that arrive as dates, so td.num would line up the
   patient numbers and leave three date columns ragged beside them. */
table.register {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--t-body);
    font-variant-numeric: tabular-nums;
}

table.register th,
table.register td {
    padding: var(--row-y) var(--s3);
    border-bottom: 1px solid var(--line);
    text-align: left;
    vertical-align: baseline;
}

table.register thead th {
    position: sticky;
    top: 0;
    z-index: 1;

    /* Opaque, or the rows read straight through the headings as they hold. Composited against the
       page rather than left transparent, exactly as the report results learnt to be. */
    background: var(--page);
    border-bottom: 0;
    box-shadow: inset 0 -1px 0 var(--line);

    font-size: var(--t-small);
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--muted);
    white-space: nowrap;
}

table.register td.num,
table.register th.num {
    text-align: right;
}

table.register td.patient-name {
    font-weight: 600;
    color: var(--ink-strong);
}

/* A document, not an app grid: the row under the pointer lifts by three and a half percent of ink
   and nothing else. No zebra — fifty horizontal bands the register does not have. */
table.register tbody tr:hover td {
    background: var(--row-hover);
}

/* The heading is the control. A link, because the sort lives in the address bar, and it fills the
   cell so the whole heading is the target rather than the word inside it. */
table.register th a.sort {
    display: inline-flex;
    align-items: center;
    gap: var(--s1);
    color: inherit;
    text-decoration: none;
}

table.register th a.sort:hover {
    color: var(--ink);
}

/* The chevron. Hidden until the column is sorted, hovered or focused, so six headings are not six
   arrows competing for the reader's attention with the one that means something. */
table.register th .chev {
    width: .5rem;
    height: .5rem;
    border-right: 1.5px solid;
    border-bottom: 1.5px solid;
    opacity: 0;
}

table.register th .chev.up {
    transform: rotate(-135deg) translate(-1px, -1px);
}

table.register th .chev.down {
    transform: rotate(45deg) translate(-1px, -1px);
}

table.register th a.sort:hover .chev,
table.register th a.sort:focus-visible .chev {
    opacity: .5;
}

/* Where the sort is, marked achromatically — full ink, weight 700, and the chevron shown. The
   design system spends colour on the one primary action and on status, and "this is the column you
   sorted by" is neither. */
table.register th.sorted {
    color: var(--ink-strong);
    font-weight: 700;
}

table.register th.sorted .chev {
    opacity: 1;
}

/* And a 3% wash of ink down the sorted column's whole length, so the marking survives a reader
   whose eye is halfway down the page. The same value the summary band uses. */
table.register th.sorted,
table.register td.sorted {
    background: color-mix(in srgb, var(--ink) 3%, transparent);
}

/* The heading's wash sits on top of its own opaque background, or the rows would read through it
   as it holds — so it is the wash composited against the page rather than the wash alone. */
table.register thead th.sorted {
    background: color-mix(in srgb, var(--ink) 3%, var(--page));
}

/* The name is the handle into one Patient's own page (SAAS-70). Underlined on hover only: fifty
   underlined names down a column is a page of rules, and the cell is already the heaviest ink in
   the row, so it reads as the thing to click without being told. */
table.register td.patient-name a.row-link {
    color: inherit;
    text-decoration: none;
}

table.register td.patient-name a.row-link:hover,
table.register td.patient-name a.row-link:focus-visible {
    text-decoration: underline;
    text-underline-offset: 2px;
}

/* --- One patient's own page (SAAS-70) ---------------------------------- */

/* Read down, not across. The register is a table because fifty rows are compared; one patient is a
   document, so this is the one Surface page that keeps the prose measure. */
.patient-detail {
    max-width: 60rem;
}

/* Back to where the reader came from, above the name rather than below the page — the browser's own
   back button is the fast path, and this is the one that survives a link somebody was sent. */
.patient-detail .crumb {
    margin: 0 0 var(--s1);
    font-size: var(--t-body);
}

.patient-detail .crumb a {
    color: var(--muted);
    text-decoration: none;
}

.patient-detail .crumb a:hover {
    color: var(--ink);
    text-decoration: underline;
}

/* The drill-downs out of a patient (SAAS-81, joined by SAAS-176's two). It was one link and is now
   up to three, so they need to be separated and to wrap rather than run together on a phone. A row
   of links and not a row of buttons: each one is a destination with an address, and the crumb above
   already established that this page navigates with links. */
.patient-detail .drill-down {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2) var(--s4);
    margin: var(--s4) 0 0;
}

.patient-detail .drill-down + .hint {
    margin: var(--s1) 0 0;
}

.detail-group {
    margin: var(--s5) 0 0;
    padding: var(--s4) var(--s5);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    background: var(--surface);
    box-shadow: var(--e1);
}

.detail-group h2 {
    margin: 0 0 var(--s3);
    font-size: var(--t-small);
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--muted);
}

/* Label beside value, and the label column sized to the longest label rather than to a guess — a
   grid whose first track is max-content cannot leave a caption wrapping over two lines beside a
   short value. Values carry tabular figures because most of them are numbers, dates or phone
   numbers read against each other down the column. */
.detail-group dl {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    gap: var(--s2) var(--s5);
    margin: 0;
    font-variant-numeric: tabular-nums;
}

.detail-group dt {
    color: var(--muted);
    font-size: var(--t-body);
}

.detail-group dd {
    margin: 0;
    color: var(--ink-strong);
    overflow-wrap: anywhere;
}

/* The staff login code beside the name a receipt's three actor fields now draw (SAAS-143). Muted and
   a step smaller, so the name is what the eye lands on and the code is there for the reader who
   needs to say it back to the desktop system or type it into the Raised by filter.

   It is a sibling of the value rather than a second `dd`: the pair is one answer to one question, and
   two `dd`s under one `dt` would put a blank cell opposite the code in the label-over-value phone
   layout above. */
.detail-group dd .staff-login {
    margin-left: var(--s2);
    color: var(--muted);
    font-size: var(--t-small);
    white-space: nowrap;
}

/* On a phone the two-column pair becomes label-over-value: an address beside a caption in a 375px
   viewport is a column four characters wide. */
@media (max-width: 767.98px) {
    .detail-group dl {
        grid-template-columns: minmax(0, 1fr);
        gap: var(--s1);
    }

    .detail-group dd {
        margin-bottom: var(--s2);
    }
}

/* The standing notes (SAAS-70, ruled 2026-08-13). Stacked blocks rather than the label-beside-value
   grid the rest of the page uses, because these are the only free text on it: `remark` is filled on
   16,156 of Puri Indah's 18,591 live rows and 10,472 of those carry a line break, so a value column
   four characters from a caption would be unreadable and the breaks would be lost. */
.patient-notes h3 {
    margin: 0 0 var(--s1);
    font-size: var(--t-body);
    font-weight: 400;
    color: var(--muted);
}

/* pre-line, not pre: the stored breaks are kept and the long lines still wrap to the column, which
   is what a 330-character note typed into a fixed-width desktop textbox needs. No tabular figures
   either — this is prose, and the rest of the page's numeric alignment would only widen its spaces. */
.patient-notes .note {
    margin: 0 0 var(--s4);
    color: var(--ink-strong);
    white-space: pre-line;
    overflow-wrap: anywhere;
}

.patient-notes .note:last-child {
    margin-bottom: 0;
}

/* --- The pager --------------------------------------------------------- */

/* One of these per paged block. On a multi-Branch Billing page there will be eight, so nothing
   here may assume it is the only one on the screen or that it sits at the foot of the page. */
.pager {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s2) var(--s4);
    margin: var(--s3) 0 0;
}

.pager .figures {
    font-variant-numeric: tabular-nums;
}

.pager p {
    margin: 0;
    font-size: var(--t-body);
    color: var(--muted);
}

.pager-pages {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s1);
    margin: 0;
    padding: 0;
    list-style: none;
    order: 2;
}

/* Every page number renders at one width per digit count, so the strip does not twitch as a reader
   moves through it. Measured in SAAS-61: six 3-digit numbers at one width with tabular figures,
   and six different widths without. */
.pager .page,
.pager .step {
    display: inline-block;
    min-width: 2rem;
    padding: var(--s1) var(--s2);
    border: 1px solid transparent;
    border-radius: var(--r-sm);
    font-size: var(--t-body);
    text-align: center;
    text-decoration: none;
    color: var(--ink);
}

.pager a.page:hover,
.pager a.step:hover {
    background: var(--row-hover);
    border-color: var(--border);
}

/* Where you are. A wash of ink, a border of ink, and weight — the same wash-and-weight idiom the
   sidebar's current page and the summary band use, and for the same reason: colour is not spent on
   which page you are on. */
.pager .page.current {
    background: color-mix(in srgb, var(--ink) 9%, transparent);
    border-color: color-mix(in srgb, var(--ink) 25%, transparent);
    font-weight: 650;
    color: var(--ink-strong);
}

/* Previous and Next at the ends of the run, and wider than a number because they are words. */
.pager .step {
    min-width: auto;
    padding-inline: var(--s2);
}

/* A step a reader cannot take is present and quiet rather than absent: the strip would otherwise
   change width between page 1 and page 2 and shift every number under the pointer. */
.pager .step[aria-disabled="true"] {
    color: var(--muted);
    opacity: .55;
}

.pager .gap {
    padding-inline: var(--s1);
    color: var(--muted);
}

.pager-count {
    order: 1;
}

.pager-where {
    order: 3;
}

/* Under the breakpoint the three parts of the pager stack rather than competing for one line, and
   the count reads above the numbers it describes. */
@media (max-width: 767.98px) {
    .pager {
        justify-content: flex-start;
    }

    .pager-pages {
        order: 3;
        width: 100%;
    }

    .pager-where {
        order: 2;
    }
}

/* -----------------------------------------------------------------------------
   A form as a dialog over its list (SAAS-115)

   Darius ruled the shape on 2026-08-22: "the entry / edit form shouldn't be below the tables — it
   should be either a new popup window or a new url", and what he took from the rendered comparison
   was the modal. Written here rather than inside .portal-people because the operator screens take
   the same convention (SAAS-120/121/124), and one sheet is the whole point.

   The class lost its realm in SAAS-121, when the Operator screens started using it: `.portal-dialog`
   is `.form-dialog`. Nothing in the block names a colour of its own — every value is a shared
   token — which is what lets an /operator page inherit the whole treatment without operator.css
   adding a line, and it is the same rule OperatorStylesheetTests holds that sheet to.

   THE TWO OPEN STATES ARE NOT THE SAME ELEMENT STATE, and that is what most of this block is
   about. `open` on its own is NOT modal: no top layer, no ::backdrop, no focus trap, and the page
   behind stays live. That is the state the server renders — a no-script open, or the round-trip
   that carries a refusal back — and it has to look finished on its own, because dialogs.js is
   polish and every write lands without it.

   So [open] fixes the panel to the viewport and draws its own scrim with a 100vmax spread shadow,
   and :modal — which showModal() produces — drops that stand-in, because ::backdrop is painting
   the real one. The one thing the no-script state cannot have is the page behind going inert; that
   is showModal()'s alone, and it is the honest cost of the baseline. */

.form-dialog {
    box-sizing: border-box;
    width: min(34rem, calc(100% - var(--s6)));
    max-height: calc(100dvh - var(--s7));
    overflow: auto;
    padding: 0;
    border: 1px solid var(--border-strong);
    border-radius: var(--r-lg);
    background: var(--surface);
    color: var(--ink);
    box-shadow: var(--e2);
}

/* THE WIDE VARIANT (SAAS-134). A form is narrow because a form field is narrow, and every dialog
   here was a column of fields until the Granted Reports dual list arrived. That control is not a
   field: its two lists have to show a report's whole name, and at 34rem each of them got about
   150px, so every name was cut mid-word — which is what Darius found in the SAAS-132 click-through.

   DARIUS RULED THE TRADE on 2026-08-24 — the wider panel, and the longest live report name renders
   whole — and 70rem is what that costs when it is measured rather than estimated. The name is
   "Kidz Dental APD VS Doctor Fee VS Treatment and Product · custom", which needs 413.5px of text.
   900px offered 328px and 1050px was the first estimate; measured, 1056px still fell 7.5px short
   and 1072px cleared it by half a pixel, which is not a margin. Per list the overhead is fixed at
   ~33px — Chrome reserves a 15px scrollbar gutter on a multi-select whether or not it scrolls, plus
   the select's 2px and the option's paddings — so the number had to come from the browser.

   1120px leaves 24.5px of headroom, about 6%, and THAT is the point of picking it over the two
   steps below: `system-ui` is a different typeface on every platform — Segoe UI Variable here, SF
   Pro on macOS — and a 2% margin is one font substitution away from clipping again on a machine
   nobody measured on.

   THE CAP IS THE SAME EXPRESSION AS THE DEFAULT WIDTH'S, deliberately: whatever this variant asks
   for, it can never be wider than the viewport less the page's own gutter, so there is no width at
   which the panel itself starts a horizontal scroll. Below about 1152px the panel is capped by that
   rather than by 70rem and the margin narrows; below the stacking breakpoint the name clips again,
   which is what the title on every option is for. */
.form-dialog.wide {
    width: min(70rem, calc(100% - var(--s6)));
}

.form-dialog[open] {
    position: fixed;
    inset-block-start: var(--s7);
    inset-inline: 0;
    margin-inline: auto;
    z-index: 40;
    box-shadow: var(--e2), 0 0 0 100vmax rgba(11, 17, 32, .55);
}

.form-dialog:modal {
    box-shadow: var(--e2);
}

.form-dialog::backdrop {
    background: rgba(11, 17, 32, .55);
}

.form-dialog .dialog-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--s4);
    padding: var(--s4);
    border-bottom: 1px solid var(--line);
}

.form-dialog .dialog-head h2 {
    margin: 0;
    font-size: var(--t-h3);
    color: var(--ink-strong);
    overflow-wrap: anywhere;
}

.form-dialog .dialog-close {
    flex: 0 0 auto;
    display: inline-flex;
    padding: var(--s1);
    border-radius: var(--r-sm);
    color: var(--muted);
}

.form-dialog .dialog-close:hover {
    color: var(--ink);
    background: var(--row-hover);
}

.form-dialog .dialog-body {
    padding: var(--s4);
}

/* The foot sits a step down from the panel, so the two actions read as the end of the form rather
   than as two more fields. Sticky against a long form scrolling inside the panel: the save button
   is the one control a reader must never have to hunt for. */
.form-dialog .dialog-foot {
    position: sticky;
    bottom: 0;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--s2);
    margin: var(--s5) calc(var(--s4) * -1) calc(var(--s4) * -1);
    padding: var(--s4);
    border-top: 1px solid var(--line);
    background: var(--page);
}

/* -----------------------------------------------------------------------------
   People and groups (SAAS-79, rebuilt as panels in SAAS-115)

   The Owner's account screen. It was two flat tables — every Group in one, every person in the
   other, and a Group column to read across — until Darius ruled layout A from a rendered
   comparison on 2026-08-22: "there is no real grouping for separations". A Group is a panel now,
   and its members are a table inside it.

   Everything reuses the tokens and the .register rules the rest of the product has. Two shapes are
   genuinely new: the grant chips, and the panel.

   ONE COLOUR IN THE MOCK DID NOT SURVIVE MEASUREMENT. The comparison drew the Owner's card on a
   4% ink wash. --muted on that wash measures 4.33:1 in light — under the floor, on the line that
   carries the Owner's own email — so the card sits on --surface here and the 3px ink edge alone
   carries the distinction. Light is this product's constrained theme (the same wash measured
   7.16:1 in dark); measure there and dark follows. */

.portal-people {
    max-width: 100%;
}

.portal-people .lead {
    max-width: var(--measure);
}

/* The Owner, outside the panels because they are outside Groups. The left edge is ink rather than
   the brand hue: this is the one card on the screen that is not a Group, and the brand is spent on
   the action. */
.portal-people .owner-card {
    display: flex;
    align-items: center;
    gap: var(--s3);
    flex-wrap: wrap;
    margin: 0 0 var(--s6);
    padding: var(--s3) var(--s4);
    border: 1px solid var(--border);
    border-inline-start: 3px solid var(--ink);
    border-radius: var(--r-sm);
    background: var(--surface);
}

.portal-people .owner-card .mono-chip {
    /* --masthead-line is a chrome token, and this chip is on the page. */
    border-color: var(--border-strong);
    background: var(--teal-wash);
    color: var(--nav-active-ink);
}

.portal-people .owner-who {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.portal-people .owner-who strong {
    color: var(--ink-strong);
}

.portal-people .owner-who .muted {
    font-size: var(--t-small);
}

.portal-people .owner-note {
    flex: 1 1 22rem;
    margin: 0;
    color: var(--muted);
    font-size: var(--t-small);
}

.portal-people .section-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s4);
    flex-wrap: wrap;
    margin: 0 0 var(--s4);
}

.portal-people .section-head h2 {
    margin: 0;
}

.portal-people .page-actions {
    display: flex;
    gap: var(--s2);
    flex-wrap: wrap;
}

/* --- One Group, one panel ---------------------------------------------- */

/* The separation the ruling asked for is a gap and an edge, not a heading. --s5 between panels is
   wider than any gap inside one, which is what makes the boundary read without a rule drawn across
   the page. */
.portal-people .group-list {
    display: flex;
    flex-direction: column;
    gap: var(--s5);
    margin: 0 0 var(--s7);
}

.portal-people .group-card {
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    background: var(--surface);
    box-shadow: var(--e1);
    overflow: hidden;
}

.portal-people .group-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--s4);
    flex-wrap: wrap;
    padding: var(--s4);
    border-bottom: 1px solid var(--line);
}

.portal-people .group-id {
    flex: 1 1 20rem;
    min-width: 14rem;
}

.portal-people .group-id h3 {
    margin: 0;
    font-size: var(--t-h3);
    color: var(--ink-strong);
}

.portal-people .group-count {
    margin: var(--s1) 0 0;
    color: var(--muted);
    font-size: var(--t-small);
}

/* All six Surfaces, always, granted or not — a list of only the granted ones cannot answer "two of
   how many". Granted wears the brand wash (--nav-active-ink on --teal-wash: 6.35:1 light, 6.74:1
   dark); ungranted is a dashed outline in --muted (4.97:1 on this surface). The state is in each
   chip's own visually-hidden text too, because a border style is not a fact anybody can hear. */
.portal-people .grant-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s1);
    margin: var(--s3) 0 0;
    padding: 0;
    list-style: none;
}

.portal-people .grant-chips li {
    padding: 2px var(--s2);
    border: 1px solid transparent;
    border-radius: 999px;
    font-size: var(--t-small);
    white-space: nowrap;
}

.portal-people .grant-chips li.on {
    border-color: var(--teal-wash);
    background: var(--teal-wash);
    color: var(--nav-active-ink);
    font-weight: 600;
}

.portal-people .grant-chips li.off {
    border-style: dashed;
    border-color: var(--border-strong);
    color: var(--muted);
}

.portal-people .group-actions {
    display: flex;
    align-items: flex-start;
    gap: var(--s2);
    flex-wrap: wrap;
}

.portal-people .group-actions form {
    margin: 0;
}

.portal-people .group-body {
    padding: var(--s2) var(--s4) var(--s4);
}

.portal-people .members-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s4);
    flex-wrap: wrap;
    padding: var(--s3) 0 var(--s2);
}

.portal-people .members-head h4 {
    margin: 0;
    font-size: var(--t-small);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
}

.portal-people .empty {
    margin: 0;
    padding: var(--s4) 0;
    color: var(--muted);
}

.portal-people .row-actions {
    display: flex;
    gap: var(--s2);
    flex-wrap: wrap;
}

.portal-people .row-actions form {
    margin: 0;
}

/* --- The invitation column, and the link shown once (SAAS-181) ---------- */

/* The row's invitation sub-state. Muted, because it is not a Standing: the column to its left
   decides whether anybody may sign in, and this one is about a link. Painting it at full strength
   would make an ordinary countdown compete with a Disabled beside it. It wraps rather than holding
   one line, because "Invited — expires in 7 days" is a clause and this table already carries six
   other columns. */
.portal-people td.invitation-state {
    white-space: normal;
    color: var(--muted);
}

/* The minted link, drawn once and never again — moved here from operator.css in SAAS-181, because
   both screens that mint one draw the same panel. It wears an ink wash rather than a colour: this
   is the outcome of an act that went through, and --ok, --warn and --bad are reserved for the
   freshness ladder and for stated failures. What sets it apart from a plain saved line is the value
   inside it, which is the whole reason the panel exists.

   Unscoped, like the dialog rules above it: it belongs to whichever screen mints a link. */
.minted-link {
    border: 1px solid var(--border-strong);
    border-inline-start: 3px solid var(--ink);
    border-radius: var(--r-sm);
    background: color-mix(in srgb, var(--ink) 4%, transparent);
    color: var(--ink);
    padding: var(--s3);
    margin: 0 0 var(--s5);
    max-width: 70ch;
}

.minted-link .minted-link-lead {
    margin: 0 0 var(--s2);
}

.minted-link .hint {
    display: block;
    margin: var(--s2) 0 0;
}

/* Whether the email went (SAAS-182), inside the panel that holds the link because both sentences
   are about that link. They are drawn differently on purpose: the success is a caption — the
   inviter need do nothing with it — while the failure hands them a job, so it carries the weight
   and the colour of a stated problem. --warn rather than --bad: the Invitation exists and the link
   beside this sentence works, so nothing here is broken. */
.minted-link .minted-link-sent {
    margin: var(--s2) 0 0;
    font-size: var(--t-small);
    color: var(--muted);
}

.minted-link .minted-link-unsent {
    margin: var(--s2) 0 0;
    font-weight: 600;
    color: var(--warn);
}

/* Full width and in the code face, because this is a value to be copied whole rather than read.
   `readonly` rather than disabled: a disabled input cannot be focused, and focus-then-select-all
   is how a link gets copied with no script and no mouse. */
.minted-link-value {
    width: 100%;
    box-sizing: border-box;
    padding: var(--s2);
    font-family: var(--mono);
    font-size: var(--t-small);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink-strong);
}

/* A refused delete lands in the panel that refused it, and its sentences name people listed a few
   rows above — so it is spaced away from the table rather than tucked under it. */
.portal-people .group-body .signin-error {
    margin: var(--s4) 0 0;
    max-width: var(--measure);
}

.portal-people .group-body .signin-error p {
    margin: 0 0 var(--s2);
}

.portal-people .group-body .signin-error p:last-child {
    margin: 0;
}

/* --- Fields, shared by every dialog on this screen --------------------- */

.portal-people .field,
.form-dialog .field {
    margin: 0 0 var(--s4);
}

.portal-people .field > label,
.form-dialog .field > label {
    display: block;
    font-size: var(--t-small);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
    margin-bottom: var(--s1);
}

.portal-people .field input,
.portal-people .field select,
.form-dialog .field input,
.form-dialog .field select {
    width: 100%;
    box-sizing: border-box;
    padding: var(--s2);
    font: inherit;
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
}

/* What a field means, under it — including the two sentences that carry the ~30-minute
   revalidation window, which is the one thing on this screen an Owner can be wrong about without
   the screen looking wrong. */
.portal-people .hint,
.form-dialog .hint,
.portal-account .hint,
.portal-accept .hint {
    display: block;
    margin: var(--s1) 0 0;
    color: var(--muted);
    font-size: var(--t-small);
    line-height: 1.55;
}

/* The grant list. Bordered because these six belong together — an Owner reads them as one answer
   to "what does this group open", not as six independent switches down a form. */
.portal-people .grants,
.form-dialog .grants {
    margin: 0 0 var(--s4);
    padding: var(--s2) var(--s4) var(--s3);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
}

.portal-people .grants legend,
.form-dialog .grants legend {
    padding: 0 var(--s1);
    font-size: var(--t-small);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
}

.portal-people .grants label,
.form-dialog .grants label {
    display: flex;
    align-items: center;
    gap: var(--s2);
    padding: var(--s1) 0;
    font-size: var(--t-body);
}

.portal-people .grants input,
.form-dialog .grants input {
    margin: 0;
    width: auto;
}

/* The Managed Branches control (SAAS-124). Same frame as the grant list above, and deliberately:
   they are the portal realm's two axes, and a reader who has ticked one should recognise the
   other. It is the fieldset's job to say the radio and the checklist are ONE answer — "only
   these" means nothing without the boxes under it. */
.form-dialog .branch-pick {
    margin: 0 0 var(--s4);
    padding: var(--s2) var(--s4) var(--s3);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
}

.form-dialog .branch-pick legend {
    padding: 0 var(--s1);
    font-size: var(--t-small);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
}

.form-dialog .branch-pick label.shape {
    display: flex;
    align-items: center;
    gap: var(--s2);
    padding: var(--s1) 0;
    font-size: var(--t-body);
}

.form-dialog .branch-pick input {
    margin: 0;
    width: auto;
}

/* Indented under "Only these:", because that is what the list belongs to. The indent is the whole
   of the relationship — no JavaScript disables these when "All branches" is chosen, since a
   disabled control with no script to re-enable it is a form that cannot be filled in. */
.form-dialog .branch-pick ul.branches-select {
    margin: 0 0 var(--s1) var(--s5);
}

/* The shape on a member row. Tabular numerals so "3 of 11" lines up down a column of them. */
.portal-people td.branch-shape {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    color: var(--muted);
}

/* Nought of eleven — the fail-closed state (SAAS-124). Nobody chose it: a release emptied the set.
   It is drawn as something to act on rather than as a smaller number, because an Owner scanning
   this panel has no other way to notice it. */
.portal-people td.branch-shape.empty {
    color: var(--bad);
    font-weight: 600;
}

/* The Doctor Identity control (SAAS-139). The same frame as the two above it, deliberately: these
   are the portal realm's three axes, and a reader who has set one should recognise the next. The
   fieldset says the radio and the per-branch picks are ONE answer — "a doctor's account" means
   nothing without the branches under it. */
.form-dialog .doctor-pick {
    margin: 0 0 var(--s4);
    padding: var(--s2) var(--s4) var(--s3);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
}

.form-dialog .doctor-pick legend {
    padding: 0 var(--s1);
    font-size: var(--t-small);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
}

.form-dialog .doctor-pick label.shape {
    display: flex;
    align-items: center;
    gap: var(--s2);
    padding: var(--s1) 0;
    font-size: var(--t-body);
}

.form-dialog .doctor-pick input[type="radio"] {
    margin: 0;
    width: auto;
}

/* Indented under "A doctor's account", for the branch checklist's reason: the indent is the whole
   of the relationship, and nothing disables these when "an ordinary account" is chosen — a
   disabled control with no script to re-enable it is a form that cannot be filled in. */
.form-dialog ul.doctor-select {
    list-style: none;
    margin: 0 0 var(--s1) var(--s5);
    padding: 0;
    /* Ten branches fit without scrolling, matching the branch checklist. Larger tenants scroll. */
    max-height: 19rem;
    overflow-y: auto;
}

/* Name on the left, picker on the right, so a column of clinic names reads down the page and a
   column of doctors reads beside it. The name gets a fixed share rather than its natural width,
   because otherwise every row's select would start somewhere different. */
.form-dialog ul.doctor-select li {
    display: grid;
    grid-template-columns: minmax(8rem, 1fr) minmax(0, 1.4fr);
    align-items: center;
    gap: var(--s3);
    padding: var(--s1) 0;
}

.form-dialog ul.doctor-select .branch {
    font-size: var(--t-body);
}

.form-dialog ul.doctor-select select {
    width: 100%;
    margin: 0;
}

/* A branch whose list could not be read. Muted rather than alarming: it is a condition of one
   clinic's host at one moment, and the link it already holds is untouched. */
.form-dialog ul.doctor-select .unreadable {
    font-size: var(--t-small);
    color: var(--muted);
}

/* The same name elsewhere (SAAS-144). Indented under the picker it fills, on the picker's own rule:
   the indent is the whole of the relationship, and this control acts on the selects above it. */
.form-dialog .also-link {
    margin: 0 0 var(--s2) var(--s5);
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s2);
}

/* What the button would add, named. Deliberately NOT styled as a notice: nothing has happened yet
   and nothing is wrong — it is a sentence about a button, so it reads the way the hints do. */
.form-dialog .also-link .offer {
    margin: 0;
    max-width: 60ch;
    font-size: var(--t-small);
    color: var(--muted);
}

/* ...and what the last press did, which IS a notice: it reports a change to the fields above. */
.form-dialog .also-link .notice {
    margin: 0;
    border: 1px solid var(--line);
    background: var(--surface);
    border-radius: var(--r-sm);
    padding: var(--s2) var(--s3);
    max-width: 60ch;
    font-size: var(--t-small);
}

/* The shape on a member row. Tabular numerals so "0 of 11" lines up under "Doctor · 2 branches". */
.portal-people td.doctor-shape {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    color: var(--muted);
}

/* A doctor's account linked nowhere (SAAS-139) — the fail-closed state on the third axis, and the
   twin of "0 of 11" one column over. Nobody chose it: a release took the last link away, and the
   account now sees nothing of its own. Drawn as something to act on, for the same reason. */
.portal-people td.doctor-shape.empty {
    color: var(--bad);
    font-weight: 600;
}

/* --- One control treatment, for links and buttons alike ---------------- */

/* Half of these actions are anchors — a no-script open is a GET of this page — and half are
   buttons on forms that write. A reader should not be able to tell which is which by looking, so
   the treatment is one class rather than two element selectors. */
.portal-people .action,
.form-dialog .action,
.portal-account .action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s2);
    padding: var(--s1) var(--s3);
    font: inherit;
    font-size: var(--t-body);
    font-weight: 500;
    text-decoration: none;
    white-space: nowrap;
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--ink);
    cursor: pointer;
}

.portal-people .action:hover,
.form-dialog .action:hover,
.portal-account .action:hover {
    border-color: var(--ink);
    background: var(--row-hover);
}

/* One filled button per dialog, and one on the page: the thing this screen is for. */
.portal-people .action.primary,
.form-dialog .action.primary {
    padding: var(--s2) var(--s4);
    font-weight: 600;
    border-color: transparent;
    background: var(--action);
    color: var(--on-action);
}

.portal-people .action.primary:hover,
.form-dialog .action.primary:hover {
    border-color: transparent;
    background: var(--action-hover);
}

/* Outlined rather than filled: a filled red on every panel would be the loudest thing on a screen
   whose subject is people. --bad on --surface measures 6.57:1 light, 7.74:1 dark. */
.portal-people .action.danger,
.form-dialog .action.danger {
    color: var(--bad);
    border-color: color-mix(in srgb, var(--bad) 45%, transparent);
}

.portal-people .action.danger:hover,
.form-dialog .action.danger:hover {
    border-color: var(--bad);
    background: color-mix(in srgb, var(--bad) 8%, transparent);
}

.portal-people .notice,
.portal-account .notice,
.portal-signin .notice {
    border: 1px solid var(--line);
    background: var(--surface);
    border-radius: var(--r-sm);
    padding: var(--s2) var(--s3);
    max-width: 60ch;
}

/* Under a phone's width the panel head stops being two columns: the actions go below the chips
   rather than beside them, which is where a thumb is anyway. */
@media (max-width: 640px) {
    .portal-people .group-actions,
    .portal-people .page-actions,
    .portal-people .row-actions {
        width: 100%;
    }

    .portal-people .members-head {
        align-items: flex-start;
        flex-direction: column;
        gap: var(--s2);
    }
}

/* --- The Billing Surface: per-Branch blocks (SAAS-71) ------------------- */

/* No cap, for the reason the report page and the register have none: this is a seven-column table
   read across, eight times over. */
.portal-billing {
    max-width: none;
}

/* The picker is a list of checkboxes rather than a multi-select, because eight branches is a list a
   reader ticks rather than a control they ctrl-click. It reflows on the same rule the register's
   search form uses: `auto-fit` with a floor, so it is four columns on a desktop and one on a phone
   with nothing declared per breakpoint. */
.branch-form {
    margin: 0 0 var(--s5);
    padding: var(--s4) var(--s5);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    background: var(--surface);
    box-shadow: var(--e1);
}

.branch-form fieldset {
    margin: 0;
    padding: 0;
    border: 0;
}

.branch-form legend {
    padding: 0;
    font-size: var(--t-small);
    font-weight: 600;
    letter-spacing: .01em;
    color: var(--muted);
}

.branch-form .branches-select {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    gap: var(--s1) var(--s4);
    margin: var(--s2) 0 0;
    padding: 0;
    list-style: none;
}

.branch-form .branches-select label {
    display: flex;
    align-items: baseline;
    gap: var(--s2);
    font-size: var(--t-body);
    color: var(--ink);
}

/* A Superseded branch is listed so an Owner is not left wondering where a clinic went, and is not
   offered (ADR 0015). Muted rather than hidden, and its reason said in words beside it. */
.branch-form .branches-select label.moved {
    color: var(--muted);
}

.branch-form .actions {
    display: flex;
    align-items: center;
    gap: var(--s4);
    margin-top: var(--s4);
}

/* Outlined, not filled â€” the register's rule: showing receipts is how a reader reaches the thing
   the page is for, not the page's one primary action. */
.branch-form button {
    padding: var(--s2) var(--s4);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    cursor: pointer;
}

.branch-form button:hover {
    background: var(--row-hover);
}

/* The filters (SAAS-72), inside the picker's own form so one submit applies both. Same grid rule as
   the register's search and the picker above it: `auto-fit` with a 14rem floor, so it is four
   columns on a desktop and one on a phone with nothing declared per breakpoint. */
.ledger-filters {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    align-items: start;
    gap: var(--s3) var(--s4);
    margin-top: var(--s4);
    padding-top: var(--s4);
    border-top: 1px solid var(--border);
}

.ledger-filters .field {
    display: flex;
    flex-direction: column;
    gap: var(--s1);
    min-width: 0;
}

/* A pair of bounds is one control with two ends, so it takes two cells where there are two to take
   and folds into one where there are not. The register's rule, unchanged. */
.ledger-filters .field-range {
    grid-column: span 2;
}

@media (max-width: 719px) {
    .ledger-filters .field-range {
        grid-column: span 1;
    }
}

.ledger-filters .range {
    display: flex;
    align-items: center;
    gap: var(--s2);
}

.ledger-filters .range input {
    flex: 1 1 0;
    min-width: 0;
}

/* THE THREE QUICK RANGES (SAAS-150), under the two date boxes they set — a doctor's merged list
   opens on today, and these are the one click that widens it to the week or the month.

   Deliberately the sort control's markings rather than a new set: both are a row of links answering
   "where are you", both live in the address bar, and a reader should not have to learn two ways of
   being told which one they are standing on. Smaller, because this sits inside one field rather
   than above the whole list. */
.quick-ranges {
    margin-top: var(--s1);
}

.quick-ranges ul {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s1);
    margin: 0;
    padding: 0;
    list-style: none;
}

.quick-ranges a.quick {
    display: inline-block;
    padding: calc(var(--s1) / 2) var(--s2);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    font-size: var(--t-small);
    text-decoration: none;
    color: var(--ink);
}

.quick-ranges a.quick:hover {
    background: var(--row-hover);
}

.quick-ranges a.quick.current {
    background: color-mix(in srgb, var(--ink) 9%, transparent);
    border-color: color-mix(in srgb, var(--ink) 25%, transparent);
    font-weight: 650;
    color: var(--ink-strong);
}

.ledger-filters .range-to {
    flex: 0 0 auto;
    font-size: var(--t-small);
    color: var(--muted);
}

/* Said once, under the control it is about. The scheme hint is the least guessable thing on this
   page and the one the ticket is about: nothing on screen otherwise distinguishes a page filtering
   on the bill's own scheme from one filtering on the patient's scheme today. */
.ledger-filters .hint {
    margin: var(--s1) 0 0;
    font-size: var(--t-small);
    line-height: 1.45;
    color: var(--muted);
}

.ledger-filters .hint strong {
    font-weight: 650;
    color: var(--ink);
}

.ledger-filters label {
    font-size: var(--t-small);
    font-weight: 600;
    letter-spacing: .01em;
    color: var(--muted);
}

.ledger-filters select,
.ledger-filters input[type="date"] {
    width: 100%;
    min-width: 0;
    padding: var(--s2);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
}

/* The open dropdown is painted by the platform, not the page - the report form's own rule. */
.ledger-filters select option {
    background: var(--surface);
    color: var(--ink);
}

/* A link, not a second button. Clearing a filter is a way back rather than an action of its own,
   and two buttons side by side would ask a reader which one they meant. */
.branch-form .actions .clear {
    font-size: var(--t-body);
    color: var(--muted);
}

.branch-form .actions .clear:hover {
    color: var(--ink);
}

/* THE EXCEL EXPORT (SAAS-75). Outside the filter form and below it, because it is a GET of another
   address rather than a second thing that form could submit. Set at the weight of the "clear
   filters" link beside it and not as a second button: the page's one action is showing receipts,
   and the export is what a reader does once they have the view they wanted. No new colour. */
.portal-billing p.export {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s2);
    margin: var(--s4) 0 var(--s5);
}

.portal-billing .export-link {
    font-size: var(--t-body);
    font-weight: 600;
    color: var(--ink);
}

/* What the blocks were narrowed to, read back. Muted, because it describes the blocks rather than
   competing with them - the same weight each pager's own count carries. */
.portal-billing .narrowed {
    margin: 0 0 var(--s4);
    font-size: var(--t-body);
    line-height: 1.5;
    color: var(--muted);
}

/* THE SURFACE'S VIEWS (SAAS-74, three of them since SAAS-157) MOVED OUT OF THIS PAGE'S BLOCK.
   Billing draws the strip and the shell places it — in the rail on a desktop, in the band under the
   masthead on a phone — so the rules that dress it are with the rest of the shell's navigation
   (SAAS-162, ADR 0027). They are not page rules any more, and leaving them here would have been a
   page reaching into chrome it no longer owns. */

/* ONE control, above every block. It is a row of links rather than the eight sets of clickable
   headings the register has, because there is one setting here and eight tables: a heading in block
   three that changed the ordering of all eight would be a control lying about its own scope. */
.sort-control {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s1) var(--s3);
    margin: 0 0 var(--s5);
}

.sort-control .sort-label {
    font-size: var(--t-small);
    font-weight: 600;
    letter-spacing: .01em;
    color: var(--muted);
}

.sort-control ul {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s1);
    margin: 0;
    padding: 0;
    list-style: none;
}

.sort-control a.sort {
    display: inline-flex;
    align-items: center;
    gap: var(--s1);
    padding: var(--s1) var(--s2);
    border: 1px solid transparent;
    border-radius: var(--r-sm);
    font-size: var(--t-body);
    text-decoration: none;
    color: var(--ink);
}

.sort-control a.sort:hover {
    background: var(--row-hover);
    border-color: var(--border);
}

/* What the blocks are sorted by. The pager's own current-page idiom, deliberately: both answer
   "where are you", both are achromatic, and a reader should not have to learn two markings. */
.sort-control a.sort.current {
    background: color-mix(in srgb, var(--ink) 9%, transparent);
    border-color: color-mix(in srgb, var(--ink) 25%, transparent);
    font-weight: 650;
    color: var(--ink-strong);
}

.sort-control .chev {
    width: .5rem;
    height: .5rem;
    border-right: 1.5px solid;
    border-bottom: 1.5px solid;
    opacity: 0;
}

.sort-control .chev.up {
    transform: rotate(-135deg) translate(-1px, -1px);
}

.sort-control .chev.down {
    transform: rotate(45deg) translate(-1px, -1px);
}

.sort-control a.sort.current .chev {
    opacity: 1;
}

.sort-control a.sort:hover .chev,
.sort-control a.sort:focus-visible .chev {
    opacity: .5;
}

/* One block per Branch, stacked. The gap is generous on purpose: eight tables with their own pagers
   have to read as eight documents rather than one long one, or an Owner will read a figure from the
   wrong clinic. */
.portal-billing .branch-block {
    margin-bottom: var(--s7);
}

.portal-billing .branch-block:last-child {
    margin-bottom: 0;
}

/* Whose receipts these are â€” the control-plane display name, never a database name. A heading with
   a rule under it, which is the report blocks' own treatment (SAAS-50): on a stack, the branch name
   is the thing that stops one block's figures being read as another's. */
.portal-billing .branch-block h2 {
    margin: 0 0 var(--s2);
    padding-bottom: var(--s1);
    border-bottom: 1px solid var(--border-strong);
    font-size: var(--t-h3);
    font-weight: 650;
    color: var(--ink-strong);
}

/* Nine columns of dates, names and money have a min-content width no phone has, so each block's
   table scrolls inside its own box and the page's own scrollWidth stays at the viewport â€” the rule
   the dashboard, the report results and the register all keep. */
.ledger-scroll {
    overflow: auto;
}

/* Capped on wide screens, which is what makes the sticky headings work at all: a sticky element is
   positioned against its nearest scroll box, and `overflow-x: auto` computes `overflow-y` to `auto`
   too, so an uncapped box is a scroll box of exactly content height and nothing can ever stick
   against it (SAAS-50's finding, re-applied). The cap is lower than the register's 70vh because
   these are stacked: a block that filled the viewport would hide the fact that there are seven
   more. */
@media (min-width: 768px) {
    .ledger-scroll {
        max-height: 55vh;
    }
}

/* Tabular figures on the whole table rather than on the money alone: a receipt number and a date are
   columns of digits too, and `td.num` would line up the money and leave those ragged beside it. */
table.ledger {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--t-body);
    font-variant-numeric: tabular-nums;
}

table.ledger th,
table.ledger td {
    padding: var(--row-y) var(--s3);
    border-bottom: 1px solid var(--line);
    text-align: left;
    vertical-align: baseline;
}

table.ledger thead th {
    position: sticky;
    top: 0;
    z-index: 1;

    /* Opaque, or the rows read straight through the headings as they hold. */
    background: var(--page);
    border-bottom: 0;
    box-shadow: inset 0 -1px 0 var(--line);

    font-size: var(--t-small);
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--muted);
    white-space: nowrap;
}

/* A column carries one alignment down its whole length, heading included â€” SAAS-50's rule, whose
   heading half had never once matched on the report page. The leading gutter is for the
   money-beside-money case: Total, Collected and Balance are three right-aligned columns in a row,
   and without it the last digit of one sits against the first of the next. */
table.ledger td.num,
table.ledger th.num {
    text-align: right;
    padding-inline-start: var(--s5);
}

table.ledger td.receipt-no,
table.ledger td.patient-name {
    color: var(--ink-strong);
}

table.ledger td.patient-name {
    font-weight: 600;
}

/* The scheme the bill was raised under (SAAS-72). Muted, and carrying no sort control of its own:
   it orders on a per-branch code whose meaning differs per branch, so ordering eight blocks by it
   would order eight different things. It reads "- Cash Patient -" for everybody at five of the
   eight Kidz branches, so it recedes rather than competing with the money. */
table.ledger td.scheme {
    color: var(--muted);
}

/* Who performed the work (SAAS-149). It holds the branch's own name for a staff login, which runs
   to 41 characters across the fleet, so it is given a floor and allowed to wrap rather than
   squeezing the money columns. It carries no sort control, for the balance's reason: it is a rule
   over a second table rather than a column. */
table.ledger td.performedby,
table.ledger th.performedby {
    min-width: 8rem;
}

/* "multiple staff" is a description sitting in a column of people's names, so it is set apart from
   them — muted and italic, the way a stated condition is everywhere else on this Surface. It is not
   a warning and takes no colour: one bill in three at Puri Indah reads this way, and a third of a
   ledger cannot be an exception. */
table.ledger td.performedby.shared {
    color: var(--muted);
    font-style: italic;
}

/* The treatment room, on the Appointments block (SAAS-77). Muted for the reason the scheme is, and
   with a stronger one behind it: the column is blank on 36 of Puri Indah's 106,722 live
   appointments, so on nearly every row it is a dash and must not compete with the booking beside it.
   It carries no sort control either — sorting one column of dashes orders nothing.

   NOT MEASURED. It reuses a token already on this table and adds no colour, which is an argument
   rather than a computed-style sweep. */
table.ledger td.room {
    color: var(--muted);
}

/* Where the blocks are sorted, marked achromatically and down the column's whole length â€” the
   register's 3% wash, which matters more here because a reader's eye is somewhere inside the fourth
   of eight tables and the control is at the top of the page. */
table.ledger th.sorted {
    color: var(--ink-strong);
    font-weight: 700;
}

table.ledger th.sorted,
table.ledger td.sorted {
    background: color-mix(in srgb, var(--ink) 3%, transparent);
}

/* The heading's wash sits on its own opaque background, or the rows read through it as it holds. */
table.ledger thead th.sorted {
    background: color-mix(in srgb, var(--ink) 3%, var(--page));
}

/* A document, not an app grid: the row under the pointer lifts by three and a half percent of ink
   and nothing else. No zebra. */
table.ledger tbody tr:hover td {
    background: var(--row-hover);
}

/* What is still owed (ruled 2026-08-13), set to agree with the state cell beside it rather than to
   compete with it: a settled balance recedes into muted exactly as "Paid" does, and one that is owed
   comes forward in full ink at the same weight as "Unpaid". The two cells are two renderings of one
   fact, so they are marked as one — achromatically, on the same argument as the states below.

   NOT MEASURED. This column shipped without a contrast sweep or a 375px scrollWidth check; it reuses
   tokens already on this table and adds no colour, which is an argument rather than a measurement. */
table.ledger td.balance.settled {
    color: var(--muted);
}

table.ledger td.balance.owing {
    color: var(--ink-strong);
    font-weight: 650;
}

/* The three payment states, told apart by weight rather than by hue.

   The palette does spend colour on status, and a payment state is one â€” but 99.95% of a clinic's
   receipts are Paid, so painting the common case would be a wall of one colour with the two states
   an Owner is actually looking for lost inside it. So Paid recedes into muted and the other two come
   forward in full ink at weight 650. Worth a ruling either way; this slice could not take one. */
table.ledger td.state {
    white-space: nowrap;
}

table.ledger td.state.paid {
    color: var(--muted);
}

table.ledger td.state.partly,
table.ledger td.state.unpaid {
    color: var(--ink-strong);
    font-weight: 650;
}

/* THE VOIDED MARKER (SAAS-155), beside the payment state and never replacing it.

   It has to come forward while the cell it shares may be receding: a voided bill reads "Paid" in
   muted ink by the rule above, and the whole point of the ruling is that "Paid" is not the whole
   story. So the marker takes full ink at the weight the two states that matter take, overriding the
   muted colour on its own span rather than repainting the cell -- the payment state underneath is
   still true and still says what it says.

   Achromatic, on this table's own argument: the states beside it spend no hue, and a marker that did
   would be the loudest thing on a page of fifty receipts for a class that is 37 of bdcwarjam's
   70,471 live bills.

   The separator is a span of its own so it can be dimmed without dimming the word.

   NOT MEASURED. Like the balance column above it, this reuses tokens already on this table and adds
   no colour, which is an argument rather than a contrast sweep. */
table.ledger td.state .void-mark {
    color: var(--ink-strong);
    font-weight: 650;
}

table.ledger td.state .void-mark .void-sep {
    color: var(--muted);
    font-weight: 400;
}

/* The receipt number is the handle into one receipt's own page (SAAS-73) — the register's rule for
   its patient names, applied to the column that is already the heaviest ink in a ledger row. Fifty
   underlined receipt numbers down a column is a page of rules, so the underline waits for hover. */
table.ledger td.receipt-no a.row-link {
    color: inherit;
    text-decoration: none;
}

table.ledger td.receipt-no a.row-link:hover,
table.ledger td.receipt-no a.row-link:focus-visible {
    text-decoration: underline;
    text-underline-offset: 2px;
}

/* --- One receipt's own page (SAAS-73) ---------------------------------- */

/* Read down, not across, exactly as one patient's page is: a ledger is a table because fifty rows
   are compared, and one bill is a document. It reuses `.crumb`, `.detail-group` and `.stated`
   rather than restating them, so a receipt and a patient read as two pages of one Surface family. */
.receipt-detail {
    max-width: 60rem;
}

.receipt-detail .crumb {
    margin: 0 0 var(--s1);
    font-size: var(--t-body);
}

.receipt-detail .crumb a {
    color: var(--muted);
    text-decoration: none;
}

.receipt-detail .crumb a:hover {
    color: var(--ink);
    text-decoration: underline;
}

/* A condition or a caveat under the thing it is about — the filters' own hint, reused so the two
   read alike. It is never an error: the commonest case it states is a voided bill, which is a fact
   about the receipt rather than a fault in the page. */
.receipt-detail .hint {
    margin: var(--s1) 0 0;
    font-size: var(--t-small);
    line-height: 1.45;
    color: var(--muted);
}

/* The priced items. The fleet's worst receipt carries 21 lines and the median carries one or two,
   so this table must hold its longest case without collapsing — it scrolls inside its own box like
   every other table on this Surface, and unlike a ledger block it is uncapped in height, because
   there is nothing stacked beneath it that a tall table would hide. */
.receipt-items .ledger-scroll {
    margin-top: var(--s1);
}

table.items {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--t-body);
    font-variant-numeric: tabular-nums;
}

table.items th,
table.items td {
    padding: var(--row-y) var(--s3);
    border-bottom: 1px solid var(--line);
    text-align: left;
    vertical-align: baseline;
}

table.items thead th {
    border-bottom: 0;
    box-shadow: inset 0 -1px 0 var(--line);
    font-size: var(--t-small);
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--muted);
    white-space: nowrap;
}

/* A column carries one alignment down its whole length, heading included. The leading gutter is
   for the money-beside-money case, as on the ledger: unit price, discount, tax and amount can be
   four right-aligned columns in a row. */
table.items td.num,
table.items th.num {
    text-align: right;
    padding-inline-start: var(--s5);
}

table.items tbody tr:last-child td {
    border-bottom: 0;
}

table.items td.item-name {
    color: var(--ink-strong);
    min-width: 14rem;
}

table.items td.amount {
    color: var(--ink-strong);
}

/* WHOSE LINE THIS IS (SAAS-149) — drawn as "Performed by", because a charged line is as often a
   product or a fee as a treatment. It sits between the item and the money, and it holds a name that
   runs to 41 characters across the fleet — "drg. Olivia Bratanata, Sp.KGA" — so it is given a floor
   and allowed to wrap rather than pushing the four money columns off a phone. It deliberately does
   not take `--ink-strong`: the item and the amount are what the eye scans down, and the staff column
   is the answer to a question the reader arrived with. */
table.items td.item-staff,
table.items th.item-staff {
    min-width: 9rem;
}

/* The login code under the name rather than beside it, which is the one place this table departs
   from the receipt header's layout above: a cell in a six-column table is already narrow and the
   name alone can wrap to two lines, so a trailing badge would set the row's height by its longest
   wrap. */
table.items td.item-staff .staff-login {
    display: block;
    margin-top: var(--s1);
    font-size: var(--t-small);
    color: var(--muted);
    font-variant-numeric: normal;
}

/* The line's own note, under the name rather than in a sixth column: it is filled on 139,800 of
   Puri Indah's 193,643 live lines, which makes it the commonest optional field on this page, and a
   column of free text would squeeze the money off a phone. Its breaks are kept because it is a
   note somebody typed, and it carries no tabular figures because it is prose. */
table.items .item-note {
    display: block;
    margin-top: var(--s1);
    font-size: var(--t-small);
    font-weight: 400;
    line-height: 1.4;
    color: var(--muted);
    font-variant-numeric: normal;
    white-space: pre-line;
    overflow-wrap: anywhere;
}

/* The money block. Right-aligned values against left-aligned labels, so the figures line up in a
   column the way they do on a printed bill — and tabular figures are already on `.detail-group dl`,
   which is what makes that alignment mean anything. */
.receipt-money dd.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.receipt-money dl {
    grid-template-columns: max-content minmax(6rem, 1fr);
}

/* Balance and State are one fact rendered twice — ReceiptPayment.BalanceOf calls .Of — so they are
   marked as one, achromatically, exactly as the ledger block marks them. A settled balance recedes
   into muted as "Paid" does; one that is owed comes forward in full ink at the same weight as
   "Unpaid".

   NOT MEASURED. This page shipped without a contrast sweep or a 375px scrollWidth check; it reuses
   tokens already on this Surface and adds no colour, which is an argument rather than a
   measurement. */
.receipt-money dd.balance.settled {
    color: var(--muted);
}

.receipt-money dd.balance.owing {
    color: var(--ink-strong);
    font-weight: 650;
}

.receipt-money dd.state.paid {
    color: var(--muted);
}

.receipt-money dd.state.partly,
.receipt-money dd.state.unpaid {
    color: var(--ink-strong);
    font-weight: 650;
}

/* On a phone the label-over-value collapse the detail grid already does would left-align the money
   again, so the values keep their own alignment down to the narrowest viewport. */
@media (max-width: 767.98px) {
    .receipt-money dl {
        grid-template-columns: minmax(0, 1fr);
    }

    .receipt-money dd.num {
        text-align: left;
    }
}

/* ---------------------------------------------------------------------------
   The Calendar View (SAAS-78) — one branch's day or week on a time axis.

   NOT MEASURED. Nobody has seen this rendered: /portal sits behind a sign-in no agent may enter and
   no gate was weakened to look. Every rule below reuses a token this stylesheet already defines and
   adds no new colour, which is an argument rather than a computed-style sweep. Treat the look as
   unverified.

   THE GEOMETRY IS NOT HERE. Where a block sits and how wide it is comes from the recreation's
   CalendarLayoutEngine as fractional rows and overlap columns, and the component turns those into
   percentages inline. This file supplies the frame, the row pitch and the weights — so making the
   grid denser is one variable, and no rule here can move a booking to the wrong time.
   --------------------------------------------------------------------------- */

/* THE ROW PITCH IS NOT DECLARED HERE (SAAS-161). It used to be `--cal-slot: 9px` on the rule below.
   CalendarBlockFloor has to know how tall a block is drawn before it can work out which blocks
   collide once the readable floor is applied, so the vertical geometry is computed in C#
   (AppointmentCalendarGrid.SlotPx) and written inline — both as each block's own top and height in
   pixels, and as `--cal-slot` on the grid itself, which is what the hour lines further down still
   read. A pitch declared here as well would be a second copy of a number the layout depends on, free
   to drift from the one the floor actually used. */
.portal-calendar {
    /* A lane narrower than this is a column of ellipses, so the day panels scroll sideways instead
       of squeezing. A branch with 71 clinicians on the roster only ever draws the ones who were in. */
    --cal-lane-min: 108px;
}

.portal-calendar .crumb {
    margin: 0 0 var(--s1);
    font-size: var(--t-body);
}

.portal-calendar .crumb a {
    color: var(--muted);
    text-decoration: none;
}

.portal-calendar .crumb a:hover {
    color: var(--ink);
    text-decoration: underline;
}

/* WHICH CLINIC'S DIARY (SAAS-169), above the diary's own navigation and built the same way — links,
   marked in weight rather than in colour, with the reading position in the address bar. Darius ruled
   the choice of clinic out of the navigation and into this page, so it is a control on the page's
   reading and is drawn as one.

   IT WRAPS RATHER THAN SCROLLS OR TRUNCATES. Nine clinics named in full is the case this has to
   carry, and the content column is not the 232px rail the captions were squeezed for — so the row
   is allowed to run onto a second line and every clinic keeps its whole name. */
.cal-branches {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s1) var(--s2);
    margin: var(--s4) 0 0;
}

.cal-branches-label {
    color: var(--muted);
    font-size: var(--t-small);
    font-weight: 600;
    letter-spacing: 0.02em;
    text-transform: uppercase;
}

/* Built exactly as `.cal-nav a` is, down to the box it becomes under a coarse pointer — the span and
   the axis switches sit a line below this row and a picker that looked like a different kind of
   control would read as one. */
.cal-branches a {
    padding: var(--s1) var(--s3);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    font-size: var(--t-body);
    text-decoration: none;
}

.cal-branches a:hover {
    border-color: var(--ink);
    color: var(--ink-strong);
}

.cal-branches a.current {
    border-color: var(--ink-strong);
    color: var(--ink-strong);
    font-weight: 650;
}

/* The rule that picked this clinic, said in words under the row — see the page for why an unstated
   choice is the one a calendar is misread by. */
.cal-one-clinic {
    margin: var(--s1) 0 0;
}

/* Previous / Today / Next, then what is on screen, then the two switches. One row on a desktop and
   wrapping on a phone, because every one of them is a link and none of them is a form. */
.cal-nav {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2) var(--s3);
    margin: var(--s4) 0 var(--s3);
}

.cal-nav a {
    padding: var(--s1) var(--s3);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    font-size: var(--t-body);
    text-decoration: none;
}

.cal-nav a:hover {
    border-color: var(--ink);
    color: var(--ink-strong);
}

/* The chosen span and axis are marked in weight rather than in colour — the palette rule this
   product keeps everywhere: colour is spent on the one primary action, not on state. */
.cal-nav a.current {
    border-color: var(--ink-strong);
    color: var(--ink-strong);
    font-weight: 650;
}

/* Which days are on screen, said in words beside the arrows so the arrows never have to be counted. */
.cal-showing {
    margin-inline-end: auto;
    color: var(--ink-strong);
    font-size: var(--t-body);
    font-weight: 600;
}

.cal-spans,
.cal-axes,
.cal-views {
    display: inline-flex;
    gap: var(--s1);
}

/* The merged calendar's mode toggle sits beside the span toggle and is built the same way — two
   links, marked in weight rather than in colour, because the palette rule spends colour on the one
   primary action and this page is already spending it on the clinics (SAAS-151). The rule above
   gives it its layout; this only separates it from the span so the two pairs do not read as four. */
.cal-views {
    padding-inline-start: var(--s3);
    border-inline-start: 1px solid var(--border);
}

/* The whole grid scrolls inside its own box, like every table on this Surface — so a week of seven
   panels never pushes the page's own scrollWidth past the viewport. */
.calendar {
    overflow: auto;
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    background: var(--surface);
    box-shadow: var(--e1);
}

.cal-frame {
    display: flex;
    align-items: flex-start;
    min-width: min-content;
}

/* The hour ruler, once for the whole visible range. It is what carries the time, which is why a
   block never prints its own end — the block's space goes to the patient's name instead. */
.cal-gutter {
    position: sticky;
    left: 0;
    z-index: 2;
    flex: 0 0 auto;
    background: var(--surface);
    border-inline-end: 1px solid var(--border);
}

/* Blank, and exactly as tall as a day's date line plus its lane headings — this is what keeps the
   ruler level with the grid beside it. */
.cal-corner {
    height: 3.05rem;
}

.cal-hour {
    position: relative;
    padding-inline: var(--s2);
    box-sizing: border-box;
    border-top: 1px solid var(--border);
    color: var(--muted);
    font-size: var(--t-micro);
    font-variant-numeric: tabular-nums;
}

.cal-hour span {
    position: relative;
    top: -.45em;
}

.cal-days {
    display: flex;
    flex: 1 1 auto;
    align-items: flex-start;
}

/* One panel per day. A day view is one of these; a week is seven, all sharing the row window the
   engine computed across the whole range, so 14:00 is at the same height in every one. */
.cal-day {
    flex: 1 1 0;
    min-width: 0;
    border-inline-end: 1px solid var(--border);
}

.cal-day:last-child {
    border-inline-end: 0;
}

.cal-date {
    margin: 0;
    padding: var(--s2) var(--s2) var(--s1);
    font-size: var(--t-small);
    font-weight: 650;
    color: var(--ink-strong);
    white-space: nowrap;
}

/* Today is marked with a dot rather than a wash: a week has one of these and six ordinary days, and
   a filled column would compete with the bookings inside it. */
.cal-day.is-today .cal-date {
    color: var(--action);
}

.cal-today {
    margin-inline-start: var(--s1);
}

.cal-lane-heads {
    display: flex;
    border-bottom: 1px solid var(--border-strong);
}

.cal-lane-head {
    flex: 1 1 0;
    min-width: var(--cal-lane-min);
    padding: var(--s1);
    overflow: hidden;
    border-inline-start: 1px solid var(--border);
    color: var(--muted);
    font-size: var(--t-micro);
    font-weight: 600;
    letter-spacing: .02em;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.cal-lane-head:first-child {
    border-inline-start: 0;
}

/* The hour lines, drawn as a gradient rather than as elements: one rule for a grid that may be 78
   rows tall, and nothing in the DOM between a reader and a booking. */
.cal-body {
    position: relative;
    display: flex;
    background-image: repeating-linear-gradient(
        to bottom,
        var(--border) 0,
        var(--border) 1px,
        transparent 1px,
        transparent calc(var(--cal-slot) * 6));
}

.cal-lane {
    position: relative;
    flex: 1 1 0;
    min-width: var(--cal-lane-min);
    border-inline-start: 1px solid var(--border);
}

.cal-lane:first-child {
    border-inline-start: 0;
}

/* One booking. Its position and width are inline, from the layout engine — an overlapping cluster of
   n splits the lane n ways and each block takes its own column, which is what "they do not cover
   each other" means. */
.cal-block {
    position: absolute;
    overflow: hidden;
    box-sizing: border-box;
    padding: 1px 4px;
    border: 1px solid var(--border-strong);
    border-inline-start: 3px solid var(--ink);
    border-radius: var(--r-sm);
    background: var(--page);
    color: var(--ink);
    font-size: var(--t-micro);
    line-height: 1.15;
}

.cal-block .cal-when {
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

.cal-block .cal-who {
    display: block;
    color: var(--ink-strong);
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.cal-block .cal-what {
    display: block;
    color: var(--muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Three weights, achromatic, the Appointments block's own map: a spent booking recedes, a live one
   is ordinary, and the two that need a reader's attention come forward. Nothing is hidden on the
   strength of its status — a cancellation is a fact about that slot. */
.cal-block.spent {
    opacity: .6;
    border-inline-start-color: var(--border-strong);
}

.cal-block.flagged {
    border-inline-start-color: var(--ink-strong);
    border-inline-start-width: 4px;
}

.cal-block.flagged .cal-who {
    text-decoration: line-through;
}

/* A booking outside the clinic's configured hours. The engine widened the window to cover it rather
   than clipping it, so this marks it as unusual and never as cut off. */
.cal-outside {
    color: var(--muted);
}

/* A booking the desk recorded no length for (SAAS-161). It used to be drawn one row tall, which
   stated the fact in a way nobody could read; now it is floored like every other block, so the block
   would otherwise claim a length it does not have. Set apart from the rest of the block, because it
   is a fact about what was recorded rather than about the appointment. */
.cal-block .cal-len {
    display: block;
    color: var(--muted);
    font-style: italic;
}

/* The blocks past the column budget, counted rather than drawn — and the count is a link to that one
   day, where the lane has the whole frame instead of a seventh of it. Nothing this grid hides goes
   uncounted. */
.cal-more {
    position: absolute;
    right: 2px;
    padding: 2px 6px;
    border: 1px dashed var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    font-size: var(--t-micro);
    font-weight: 600;
    text-decoration: none;
}

.cal-more:hover {
    border-color: var(--ink);
    border-style: solid;
}

.cal-empty {
    margin: var(--s2);
    color: var(--muted);
    font-size: var(--t-small);
}

/* ---------------------------------------------------------------------------
   THE MERGED OWN CALENDAR'S TWO RENDERINGS (SAAS-151).

   Darius ruled both from a rendered comparison of the real fixture week: the GRID (.mw-) replaced
   the per-Branch-lane grid outright, and the AGENDA (.ma-) is the second mode beside it.

   WHAT IS NOT HERE, AND WHY. The grid's ROW PITCH is not in this file. CalendarBlockFloor has to
   know how tall a block is drawn before it can work out which blocks collide once the readable
   floor is applied, so the vertical geometry is computed in C# (MergedCalendarLook.SlotPx) and
   written inline in pixels. A pitch declared here as well would be a second copy of a number the
   layout depends on.

   WHAT *IS* HERE IS THE WIDTH BUDGET, and it is the thing the old rendering got wrong. --mw-lane-min
   is the day column's minimum, the lane fills exactly one day column, and .mw-day carries no
   `min-width: 0` — which is what makes .mw-frame's min-content real, so a viewport too narrow for
   seven columns scrolls the calendar box instead of letting a column overflow into its neighbour.
   MergedWeekBudgetTests reads --mw-lane-min back out of this file and checks 7 columns plus the
   gutter against 1280px. Change the number here and that test is what tells you.
   --------------------------------------------------------------------------- */

.portal-calendar {
    /* The narrowest a day column may be drawn before the week scrolls sideways. Seven of these
       plus the 46px gutter is 1096px, inside the 1280px the ruling names. */
    --mw-lane-min: 150px;

    /* A day with nothing on it gives its width to the days that have something. It still says
       "Nothing booked" — narrowed, not removed. */
    --mw-blank: 78px;
}

/* WHICH HUE IS WHICH CLINIC. It sits above both renderings, because a reader who switches modes
   should not have to relearn the key. */
.mw-legend {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2) var(--s4);
    margin: 0 0 var(--s3);
    padding: var(--s2) var(--s3);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    background: var(--surface);
    font-size: var(--t-small);
}

.mw-legend-label {
    color: var(--muted);
    font-size: var(--t-micro);
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
}

.mw-legend-item {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
}

.mw-swatch {
    flex: 0 0 auto;
    width: 10px;
    height: 10px;
    border-radius: 3px;
    background: var(--branch);
}

/* The code carries the hue as ink as well as a swatch: at 10px the swatch alone is a colour, and a
   reader distinguishing six of them needs the letters to be the thing they actually read. */
.mw-code {
    color: var(--branch);
    font-size: var(--t-micro);
    font-weight: 750;
    letter-spacing: .04em;
}

.mw-legend-name {
    color: var(--ink-strong);
}

.mw-legend-count {
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

/* --- The grid: one lane per day ---------------------------------------- */

.mw-frame {
    display: flex;
    align-items: flex-start;
    min-width: min-content;
}

/* The hour ruler. Its width is written inline from the same C# constant the blocks are placed
   with, so the ruler and the pitch cannot drift apart. */
.mw-gutter {
    position: sticky;
    left: 0;
    z-index: 2;
    flex: 0 0 auto;
    background: var(--surface);
    border-inline-end: 1px solid var(--border);
}

/* Blank, and exactly as tall as a day's date line plus its count line — this is what keeps the
   ruler level with the grid beside it.

   IT IS ARITHMETIC ON THE SAME TOKENS THE TWO LINES ARE BUILT FROM, not a rem somebody eyeballed.
   The one this replaced was 3.35rem against a 52.25px band and put the hour ruler 1.3px below the
   grid — a drift small enough to survive review and large enough to see on a 78-row grid, and it was
   found by measuring rather than by reading. The line-heights are pinned just below for the same
   reason: the sum is only exact if nothing upstream can change them. */
.mw-corner {
    height: calc(
        var(--s2)                    /* .mw-date padding-top */
        + var(--t-small) * 1.5       /* .mw-date line box */
        + var(--s1) * 2              /* .mw-count padding, top and bottom */
        + var(--t-micro) * 1.5       /* .mw-count line box */
        + 1px);                      /* .mw-count border-bottom */
}

.mw-hour {
    position: relative;
    padding-inline: var(--s2);
    box-sizing: border-box;
    border-top: 1px solid var(--border);
    color: var(--muted);
    font-size: var(--t-micro);
    font-variant-numeric: tabular-nums;
}

.mw-hour span {
    position: relative;
    top: -.45em;
}

.mw-days {
    display: flex;
    flex: 1 1 auto;
    align-items: flex-start;
}

/* ONE PANEL PER DAY, AND ITS OWN MINIMUM IS THE LANE'S MINIMUM. No `min-width: 0` here: that is
   exactly what stopped .mw-frame from ever reaching min-content in the rendering this replaced, so
   no scrollbar appeared and the lanes silently overflowed into the next day. */
.mw-day {
    flex: 1 1 0;
    min-width: var(--mw-lane-min);
    border-inline-end: 1px solid var(--border);
}

.mw-day:last-child {
    border-inline-end: 0;
}

/* A day with nothing booked narrows to a strip, which is where most of the week's readability came
   from: the booked days get the width the empty ones were spending on blank grid. */
.mw-day.is-blank {
    flex: 0 0 var(--mw-blank);
    min-width: var(--mw-blank);
}

.mw-date {
    margin: 0;
    padding: var(--s2) var(--s2) 0;
    font-size: var(--t-small);
    line-height: 1.5;
    font-weight: 650;
    color: var(--ink-strong);
    white-space: nowrap;
}

.mw-day.is-blank .mw-date {
    color: var(--muted);
    font-weight: 600;
}

/* Today is marked with a dot rather than a wash: a week has one of these and six ordinary days, and
   a filled column would compete with the bookings inside it. */
.mw-day.is-today .mw-date {
    color: var(--action);
}

.mw-today {
    margin-inline-start: var(--s1);
}

/* How many bookings this day holds, where the clinic column headings used to be. It occupies the
   same band so the ruler stays level, and it answers the question the headings used to. */
.mw-count {
    margin: 0;
    padding: var(--s1) var(--s2);
    border-bottom: 1px solid var(--border-strong);
    color: var(--muted);
    font-size: var(--t-micro);
    line-height: 1.5;
    font-weight: 600;
    letter-spacing: .02em;
    white-space: nowrap;
}

.mw-none {
    margin: var(--s2);
    color: var(--muted);
    font-size: var(--t-micro);
}

/* The hour lines, drawn as a gradient rather than as elements: one rule for a grid that may be 78
   rows tall, and nothing in the DOM between a reader and a booking. The 54px period is six
   ten-minute rows at the 9px pitch MergedCalendarLook.SlotPx declares. */
.mw-body {
    position: relative;
    display: flex;
    background-image: repeating-linear-gradient(
        to bottom,
        var(--border) 0,
        var(--border) 1px,
        transparent 1px,
        transparent 54px);
}

/* EXACTLY ONE OF THESE PER DAY, filling its day column. That is the whole of the lane-overflow fix:
   there is no second lane to push past the day's right edge. */
.mw-lane {
    position: relative;
    flex: 1 1 auto;
    width: 100%;
    min-width: 0;
}

/* One booking. Its top and height are inline in pixels from CalendarBlockFloor — already floored,
   and already re-columned at the floored height, which is why two short bookings 20 minutes apart
   split the lane here where their real intervals never overlapped. */
.mw-block {
    position: absolute;
    overflow: hidden;
    box-sizing: border-box;
    padding: 3px var(--s2) 3px 6px;
    border: 1px solid var(--border-strong);
    border-inline-start: 4px solid var(--branch, var(--ink));
    border-radius: var(--r-sm);
    background: var(--page);
    color: var(--ink);
    font-size: var(--t-micro);
    line-height: 1.25;
}

/* Time, clinic and length on one line, in that order: the time is what a diary is scanned down,
   the clinic is what changes between the blocks, and the length is the smallest of the three. */
.mw-top {
    display: flex;
    align-items: baseline;
    gap: var(--s1);
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

.mw-when {
    font-weight: 600;
}

/* A booking the desk recorded with no length at all. It says so in words rather than being drawn
   as a sliver — there are two in the proved week, and the ticket that raised this slice had only
   counted one because the other was nine pixels tall. Ink rather than a status hue: it is a gap in
   what was recorded, not a warning about the appointment. */
.mw-len.is-none {
    color: var(--ink);
    font-style: italic;
}

.mw-who {
    display: block;
    margin-top: 1px;
    color: var(--ink-strong);
    font-weight: 650;
    overflow: hidden;
}

.mw-what {
    display: block;
    color: var(--muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Three weights, achromatic, the Appointments block's own map — unchanged by this slice, and
   deliberately NOT the Branch hue's job. The clinic is an identity and the status is a state, and a
   reader has to be able to read both off one block. */
.mw-block.spent {
    opacity: .62;
}

.mw-block.flagged {
    border-inline-start-style: double;
    border-inline-start-width: 6px;
}

.mw-block.flagged .mw-who {
    text-decoration: line-through;
}

/* A booking outside the clinic's configured hours. The engine widened the window to cover it rather
   than clipping it, so this marks it as unusual and never as cut off. */
.mw-outside {
    color: var(--muted);
}

/* MORE BOOKINGS THAN THE COLUMN BUDGET ALLOWS, counted rather than drawn — and a link, because
   nothing may be hidden without somewhere to go. It goes to that one day, which has seven times the
   width the week gave it. */
.mw-more {
    position: absolute;
    right: 2px;
    padding: 2px 6px;
    border: 1px dashed var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    font-size: var(--t-micro);
    font-weight: 600;
    text-decoration: none;
}

.mw-more:hover {
    border-color: var(--ink);
    border-style: solid;
}

/* --- The agenda: seven day rows, no axis -------------------------------- */

.ma {
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    background: var(--surface);
    box-shadow: var(--e1);
}

.ma-day {
    display: flex;
    align-items: stretch;
    border-bottom: 1px solid var(--border);
}

.ma-day:last-child {
    border-bottom: 0;
}

.ma-date {
    flex: 0 0 11rem;
    padding: var(--s3) var(--s3) var(--s3) var(--s4);
    border-inline-end: 1px solid var(--border);
    color: var(--ink-strong);
    font-size: var(--t-small);
    font-weight: 650;
    white-space: nowrap;
}

.ma-dow {
    display: block;
    color: var(--muted);
    font-size: var(--t-micro);
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
}

.ma-day.is-today .ma-date {
    color: var(--action);
}

.ma-today {
    display: block;
    color: var(--action);
    font-size: var(--t-micro);
    font-weight: 600;
}

.ma-rows {
    flex: 1 1 auto;
    min-width: 0;
}

/* AN EMPTY DAY COSTS ONE LINE. On the grid it costs a column of blank rows; here it costs a
   sentence, which is most of what this mode buys. */
.ma-none {
    margin: 0;
    padding: var(--s3) var(--s4);
    color: var(--muted);
    font-size: var(--t-small);
}

/* One booking, full width. NOTHING TRUNCATES: the patient's name wraps rather than ellipsising,
   which is the whole reason this mode exists beside the grid. */
.ma-row {
    display: flex;
    align-items: baseline;
    gap: var(--s3);
    padding: var(--row-y) var(--s4);
    border-bottom: 1px solid var(--line);
}

.ma-row:last-child {
    border-bottom: 0;
}

.ma-row:hover {
    background: var(--row-hover);
}

.ma-time {
    flex: 0 0 8rem;
    color: var(--ink-strong);
    font-size: var(--t-small);
    font-weight: 650;
    font-variant-numeric: tabular-nums;
}

/* THE DURATION IN WORDS, because there is no axis to show it. "25 minutes · to 13:55" — the length
   and the end, since a diary is read for both. */
.ma-len {
    display: block;
    color: var(--muted);
    font-size: var(--t-micro);
    font-weight: 500;
}

/* The same fact the grid states in two words, with room here for the sentence. */
.ma-len.is-none {
    color: var(--ink);
    font-style: italic;
}

.ma-branch {
    flex: 0 0 5.5rem;
    display: flex;
    align-items: center;
    gap: var(--s2);
}

.ma-swatch {
    flex: 0 0 auto;
    align-self: stretch;
    width: 3px;
    min-height: 18px;
    border-radius: 2px;
    background: var(--branch);
}

.ma-who {
    flex: 1 1 auto;
    min-width: 0;
    color: var(--ink-strong);
    font-size: var(--t-body);
    font-weight: 600;
}

.ma-what {
    display: block;
    color: var(--muted);
    font-size: var(--t-small);
    font-weight: 400;
}

.ma-status {
    flex: 0 0 auto;
    color: var(--muted);
    font-size: var(--t-micro);
}

.ma-row.spent {
    opacity: .66;
}

.ma-row.flagged .ma-who {
    text-decoration: line-through;
}

/* HOW LONG THE DOCTOR WAS FREE, written between the rows. On a time axis this is a visible gap; with
   no axis it has to be a sentence, or the agenda loses the one thing the grid is better at. */
.ma-gap {
    margin: 0;
    padding: 3px var(--s4) 3px 6.5rem;
    border-bottom: 1px solid var(--line);
    color: var(--muted);
    font-size: var(--t-micro);
    font-style: italic;
}

/* --- The Branch hues, resolved once per tone --------------------------- */

/* Six classes, one custom property. Every rule above paints with var(--branch), so a component
   never names a colour and adding a seventh Branch hue is one pair of lines here. */
.tone-1 { --branch: var(--branch-1); }
.tone-2 { --branch: var(--branch-2); }
.tone-3 { --branch: var(--branch-3); }
.tone-4 { --branch: var(--branch-4); }
.tone-5 { --branch: var(--branch-5); }
.tone-6 { --branch: var(--branch-6); }

/* WHAT EACH CLINIC CONTRIBUTED to a merged own week (SAAS-142), including the ones that contributed
   nothing. It reads as a list of clinics rather than as a table, because the interesting rows are
   the ones carrying a sentence rather than a number. */
.cal-sites {
    margin: var(--s4) 0 0;
    padding: 0;
    list-style: none;
    border-top: 1px solid var(--line);
}

.cal-sites li {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s1) var(--s3);
    align-items: baseline;
    padding: var(--s2) 0;
    border-bottom: 1px solid var(--line);
    font-size: var(--t-small);
}

.cal-site-name {
    min-width: 14rem;
    color: var(--ink-strong);
    font-weight: 600;
}

.cal-site-count {
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

/* A stated condition — "no column in your name", "you are not recorded as a doctor here". It carries
   the weight of ordinary ink rather than of a warning: neither is a fault, and one of them is the
   Owner's to act on. */
.cal-site-said {
    flex: 1 1 20rem;
    color: var(--ink);
}

.portal-calendar .hint {
    margin: var(--s2) 0 0;
    color: var(--muted);
    font-size: var(--t-small);
}

/* Where a block's Calendar View is reached from, inside that branch's own block on the lists page. */
.portal-billing .branch-block .block-link {
    margin: 0 0 var(--s2);
    font-size: var(--t-body);
}

@media (max-width: 767px) {
    /* A phone reads one day, not seven, so the lanes may be narrower than a desktop's before they
       become a column of ellipses. The merged grid's own minimum is NOT relaxed here: at 150px it
       already fits seven columns inside 1280px, and a phone reading the merged week scrolls the
       calendar box sideways rather than squeezing a booking into 84px. */
    .portal-calendar {
        --cal-lane-min: 84px;
    }

    /* The agenda stacks instead: the date stops being a column and becomes the row above, which is
       what keeps a full patient name unwrapped on the narrowest screen this product meets. */
    .ma-day {
        display: block;
    }

    .ma-date {
        border-inline-end: 0;
        border-bottom: 1px solid var(--border);
        padding: var(--s2) var(--s4);
    }

    .ma-dow {
        display: inline;
        margin-inline-end: var(--s2);
    }

    .ma-row {
        flex-wrap: wrap;
        gap: var(--s1) var(--s3);
    }

    .ma-gap {
        padding-inline-start: var(--s4);
    }
}

/* --- Where a refused reader is told why (SAAS-84) ---------------------- */

/* This page introduces no look of its own, and that is deliberate. Its whole content is an `h1` and
   two `p.stated` sentences, which the page shell and the existing statement rule already draw — the
   same treatment a refused patient page or receipt gets, so a refusal reads the same wherever the
   reader meets one. What is here is the crumb, which is scoped per page throughout this file rather
   than declared once, and is copied rather than folded into those three so that no existing page's
   look changes on the strength of a new one arriving. */
.portal-refusal {
    max-width: 60rem;
}

.portal-refusal .crumb {
    margin: 0 0 var(--s1);
    font-size: var(--t-body);
}

.portal-refusal .crumb a {
    color: var(--muted);
    text-decoration: none;
}

.portal-refusal .crumb a:hover {
    color: var(--ink);
    text-decoration: underline;
}

/* ================================================================================================
   THE DAY END SURFACE (SAAS-95)

   The portal's sixth Surface, and the only page here whose whole subject is money. Two shapes: a
   FLEET MONEY BAND — one row per branch over Sales, Collections and Money Received, with the fleet
   total last — over PER-BRANCH BLOCKS that are the full documents, collapsed by default.

   Everything below is scoped under `.portal-dayend`. The page reuses the report page's controls
   verbatim (`.report-form`, `.select-all`, `ul.branches-select`, `.run`, `.running`, `.spinner`,
   `p.stated`), so nothing about picking a period or a branch is decided a second time here.
   ============================================================================================= */

.portal-dayend {
    max-width: 78rem;
}

/* The document proper, as against the controls above it. It carries the masthead and everything
   the print path emits — which is why it is a container at all: `@media print` reaches the
   document by hiding the chrome around it. */
.portal-dayend .dayend-document {
    margin-top: var(--s6);
}

.portal-dayend .doc-masthead {
    padding-bottom: var(--s3);
    border-bottom: 1px solid var(--border-strong);
    margin-bottom: var(--s5);
}

.portal-dayend .doc-masthead .t {
    font-size: var(--t-h2);
    font-weight: 700;
    letter-spacing: -.01em;
    color: var(--ink-strong);
}

.portal-dayend h2 {
    font-size: var(--t-h3);
    margin: var(--s6) 0 var(--s2);
    color: var(--ink-strong);
}

.portal-dayend h3 {
    font-size: var(--t-body);
    font-weight: 650;
    margin: var(--s5) 0 var(--s2);
    color: var(--ink-strong);
}

/* Every wide table scrolls INSIDE ITS OWN BOX rather than pushing the page sideways. That is the
   375px requirement stated as a rule: the billing detail is four columns of figures and cannot be
   made narrow, so the box scrolls and the document stays still. */
.portal-dayend .tblbox {
    overflow-x: auto;
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    background: var(--surface);
}

.portal-dayend table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--t-small);
}

.portal-dayend th,
.portal-dayend td {
    padding: var(--row-y) var(--s3);
    text-align: left;
    border-bottom: 1px solid var(--border);
    vertical-align: top;
}

.portal-dayend thead th {
    font-weight: 600;
    font-size: var(--t-micro);
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--muted);
    background: var(--code-surface);
    white-space: nowrap;
}

/* Tabular figures, right-aligned. The band puts three number columns side by side, and without
   tabular numerals the rupiah figures in them do not line up to be compared — which is the one
   thing the band exists for. */
.portal-dayend .num {
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.portal-dayend tbody tr:last-child td,
.portal-dayend tbody tr:last-child th {
    border-bottom: 0;
}

/* A total is a different KIND of row, not a bolder one: it is ruled off above and sits on a wash,
   so a reader scanning a column of figures can see where the column stops. */
.portal-dayend tr.total > * {
    font-weight: 650;
    color: var(--ink-strong);
    background: var(--code-surface);
    border-top: 1px solid var(--border-strong);
}

.portal-dayend tr.total.fleet > * {
    background: var(--teal-wash);
    color: var(--nav-active-ink);
}

/* ------------------------------------------------------------------ the band */

.portal-dayend .dayend-band {
    margin-top: var(--s2);
}

.portal-dayend table.band th[scope="row"] {
    font-weight: 500;
    color: var(--ink);
}

/* A stated condition is WORDS IN A FIGURE COLUMN, and this is the rule that keeps them readable
   there. It relaxes nowrap and the tabular figures — a sentence set in tabular numerals on one
   line would run the band off the page — and drops to the muted ink, because the one thing a
   withheld figure must never look like is a number. */
.portal-dayend .num .stated,
.portal-dayend td.stated {
    display: inline-block;
    max-width: 34ch;
    white-space: normal;
    text-align: left;
    font-variant-numeric: normal;
    font-weight: 400;
    color: var(--muted);
}

/* MEASURED, not assumed. The muted ink above reads at 4.41:1 on the fleet row -- under the 4.5:1
   bar -- because that row alone sits on --teal-wash rather than on the page. The harness caught it;
   nobody would have seen it, because the one place it appears is a fleet Sales figure that has been
   withheld, which is the rarest state on the document and the most important sentence on it.

   --nav-active-ink is the token that exists for exactly this pair: "the ink that reads on
   --teal-wash", declared once and answering for both themes. And the muted ink was carrying no
   meaning here that survives its removal -- what tells a reader this is words rather than a figure
   is that it wraps, ranges left, and drops the tabular numerals, all of which the rule above still
   does. */
.portal-dayend tr.total.fleet .stated {
    color: var(--nav-active-ink);
}

/* ------------------------------------------------------------------ the blocks */

/* <details>, so a block survives with no circuit, is keyboard-operable without any aria of ours,
   and can be forced open for printing by setting one attribute. A scripted panel would need all
   three built. */
.portal-dayend .dayend-block {
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    background: var(--surface);
    margin-bottom: var(--s3);
    box-shadow: var(--e1);
}

.portal-dayend .dayend-block > summary {
    padding: var(--s3) var(--s4);
    cursor: pointer;
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    align-items: baseline;
    border-radius: var(--r-md);
}

.portal-dayend .dayend-block > summary:hover {
    background: var(--row-hover);
}

.portal-dayend .dayend-block > summary:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: -2px;
}

.portal-dayend .dayend-block[open] > summary {
    border-bottom: 1px solid var(--border);
    border-radius: var(--r-md) var(--r-md) 0 0;
}

.portal-dayend .block-name {
    font-weight: 650;
    color: var(--ink-strong);
}

.portal-dayend .block-body {
    padding: var(--s2) var(--s4) var(--s5);
}

.portal-dayend .block-body > h3:first-child {
    margin-top: var(--s3);
}

/* ------------------------------------------------------------------ the headline tiles */

.portal-dayend .tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
    gap: var(--s3);
    margin: var(--s4) 0 var(--s2);
}

.portal-dayend .tile {
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    padding: var(--s3);
    background: var(--page);
}

.portal-dayend .tile .lbl {
    font-size: var(--t-micro);
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--muted);
}

.portal-dayend .tile .val {
    font-size: var(--t-h3);
    font-weight: 650;
    font-variant-numeric: tabular-nums;
    margin-top: var(--s1);
    color: var(--ink-strong);
}

/* A withheld figure in a tile. It keeps the tile's size and weight so the row of five still reads
   as one rank of headline figures — but it drops the tabular numerals, which exist to make digits
   line up and do exactly nothing for a phrase, and takes the muted ink so it cannot be mistaken at
   a glance for an amount. "Not given" and "0" are different claims; this is what keeps them
   looking different. */
.portal-dayend .tile .val.stated {
    font-variant-numeric: normal;
    color: var(--muted);
}

/* The grain, under every figure. D10 was a report that never declared what a row was, and this
   line is the correction: the number says how much, and this says of what. */
.portal-dayend .tile .sub {
    margin-top: var(--s1);
    font-size: var(--t-micro);
    color: var(--muted);
}

/* ------------------------------------------------------------------ the roll-ups */

.portal-dayend .rolls {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
    gap: var(--s4);
    margin-top: var(--s5);
}

.portal-dayend .rollup .cap {
    font-size: var(--t-small);
    font-weight: 650;
    color: var(--ink-strong);
    margin-bottom: var(--s2);
}

.portal-dayend .note {
    margin: var(--s2) 0 0;
    font-size: var(--t-micro);
    color: var(--muted);
    max-width: var(--measure);
}

/* ------------------------------------------------------------------ the billing detail */

/* A bill, its treatment lines and its payments — the bill appearing ONCE. The indent carries the
   nesting, because the table is deliberately flat: three row classes at three depths rather than
   three tables, so a reader follows one bill down the page without the eye leaving the column of
   amounts. The legacy repeated the whole item list under every payment (D5); this does not. */
.portal-dayend tr.billrow > td {
    font-weight: 600;
    color: var(--ink-strong);
    border-top: 1px solid var(--border-strong);
}

.portal-dayend tr.itemrow > td:nth-child(2),
.portal-dayend tr.payrow > td:nth-child(2) {
    padding-left: var(--s5);
}

.portal-dayend tr.payrow > td {
    color: var(--muted);
}

.portal-dayend .ref {
    font-family: var(--mono);
    font-size: var(--t-micro);
}

/* ------------------------------------------------- hidden sections, said out loud */

/* Empty sections hide, with one line saying so (SAAS-62's precedent, carried onto the money
   document). The dashed rule is the point of the treatment: it reads as a place where something
   is NOT, which is exactly what the sentence inside it is telling the reader. */
.portal-dayend .empty-say {
    margin: var(--s5) 0 0;
    padding: var(--s3);
    border: 1px dashed var(--border-strong);
    border-radius: var(--r-md);
    color: var(--muted);
    font-size: var(--t-small);
    max-width: var(--measure);
}

.portal-dayend p.block-refusal,
.portal-dayend p.sales-condition {
    max-width: var(--measure);
}

.portal-dayend p.sales-condition {
    margin-top: var(--s3);
}

/* ================================================================================================
   PRINTING THE DAY END (SAAS-95)

   Ruled 2026-08-18 from three built prototypes: THE PAGE PRINTS, and the browser's own Save as PDF
   is the PDF path. No server PDF pipeline is built — the generated-PDF prototype produced output
   identical to the browser's own, so the pipeline would buy nothing. It returns only if day-ends
   must ever be produced unattended.

   Five things happen here, and each one is a defect seen in the prototypes:

     1. THE CHROME GOES. Header, sidebar, footer, the controls, the lead. A printed day-end that
        carries a navigation rail is not a document.
     2. THE SCROLL BOXES OPEN. `overflow-x: auto` clips at the paper's edge instead of scrolling,
        so a wide table would print with columns simply missing and nothing on the sheet to say so.
     3. HEADINGS REPEAT PER SHEET. `display: table-header-group` on thead — a billing detail
        running four pages is unreadable from page two without its column names.
     4. ROWS DO NOT SPLIT. `break-inside: avoid` at the ROW, not the table: forbidding the table to
        break would push a long one onto a fresh sheet and leave half a page blank.
     5. BACKGROUNDS SURVIVE. `print-color-adjust: exact` on the rows whose meaning IS their fill —
        totals, the fleet row, the tiles. Browsers drop backgrounds by default, and a total that
        prints identically to the rows above it has stopped being a total.

   THE LIGHT THEME IS NOT FORCED HERE, and that is the design. print.js stamps
   `data-theme="light"` on <html> before the print and removes it after, which switches the dark
   block off and lets the light tokens already declared in `:root` apply. That is why this block
   declares no colour of its own: there is one palette in this product, and printing SELECTS from
   it rather than adding to it.
   ============================================================================================= */

@media print {
    /* The document alone. Everything structural that is not the day-end goes: the masthead, because
       it is sticky on screen and on paper would repeat at the top of every sheet, and the
       navigation, because a rail of links is the one thing paper can do nothing with. The rail came
       back in SAAS-162 and this list came back with it — it is chrome, and print takes none. */
    .site-header,
    .shell-nav,
    .portal-footer,
    .no-print {
        display: none !important;
    }

    /* The shell's frame is a screen affordance: on paper the margin is the printer's, and the
       side-by-side arrangement has nothing left beside it. */
    .shell-body {
        display: block;
    }

    .site-main,
    .portal-dayend {
        display: block;
        max-width: none;
        margin: 0;
        padding: 0;
    }

    body {
        background: var(--page);
        color: var(--ink);
    }

    /* Blocks are force-expanded by print.js setting `open`. This removes the affordance that no
       longer means anything on paper, and the card treatment that would draw a box round every
       branch of a fleet. */
    .portal-dayend .dayend-block > summary {
        list-style: none;
        cursor: auto;
    }

    .portal-dayend .dayend-block > summary::-webkit-details-marker {
        display: none;
    }

    .portal-dayend .dayend-block {
        box-shadow: none;
        border-radius: 0;
        border-width: 1px 0 0;
        margin-bottom: 0;
    }

    /* (2) — the clip that would silently drop columns. */
    .portal-dayend .tblbox {
        overflow: visible;
        border-radius: 0;
    }

    /* (3) and (4). */
    .portal-dayend thead {
        display: table-header-group;
    }

    .portal-dayend tr {
        break-inside: avoid;
    }

    /* A branch starts on its own sheet. Eight documents flowing into one another was the legacy's
       own printed failure, and the one thing an owner does with these is hand one branch's pages
       to one branch's manager. */
    .portal-dayend .dayend-block {
        break-before: page;
    }

    .portal-dayend .dayend-block:first-of-type {
        break-before: auto;
    }

    .portal-dayend h2,
    .portal-dayend h3 {
        break-after: avoid;
    }

    /* (5). */
    .portal-dayend tr.total > *,
    .portal-dayend tr.total.fleet > *,
    .portal-dayend .tile,
    .portal-dayend thead th,
    .portal-dayend .empty-say {
        -webkit-print-color-adjust: exact;
        print-color-adjust: exact;
    }
}

/* ---------------------------------------------------------------------------
   Granted Reports: the dual list inside Edit grants (SAAS-132, ADR 0024 ruling 5)

   Darius ruled variant B from a rendered comparison on 2026-08-23 — available on the left, granted
   on the right — with three refinements taken here: BOTH LISTS LEVEL, each with its OWN search box,
   and grouped by Report Category. Level is the load-bearing one: in the render only the left list
   had a search box, so the right one's select started higher and the pair read as a control and its
   annexe rather than as two halves of one question. Same box above each, same height below, and the
   eye compares them.

   The whole block is `hidden` when Reports is unticked, by the server on a submit and by dialogs.js
   the instant the tick moves. `hidden` rather than absent so script has something to reveal, and
   `display: none` is left to the browser's own `[hidden]` rule. */
.form-dialog .report-grants {
    margin: 0 0 var(--s4);
}

.form-dialog .report-shape {
    margin: 0;
    padding: var(--s2) var(--s4) var(--s3);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
}

.form-dialog .report-shape legend {
    padding: 0 var(--s1);
    font-size: var(--t-small);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
}

.form-dialog .report-shape label.shape {
    display: flex;
    align-items: center;
    gap: var(--s2);
    padding: var(--s1) 0;
    font-size: var(--t-body);
}

.form-dialog .report-shape label.shape input {
    margin: 0;
    width: auto;
}

/* "5 of 28", here and on the panel chip. Tabular numerals so the count does not jitter as it
   changes under a reader who is clicking the arrows. */
.form-dialog .report-shape .tally,
.portal-people .grant-chips .tally {
    font-variant-numeric: tabular-nums;
}

.form-dialog .report-shape .tally {
    color: var(--muted);
    font-size: var(--t-small);
}

/* Three columns: list, arrows, list. `1fr auto 1fr` is what keeps the two lists equal however long
   a report's name is — the arrows take what they need and the rest is halved. `align-items:
   stretch` is what keeps them level when one column's search box wraps to two lines. */
.form-dialog .dual {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    gap: var(--s3);
    align-items: stretch;
    margin: var(--s2) 0 0 var(--s5);
}

.form-dialog .dual .col {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.form-dialog .dual .col h4 {
    margin: 0 0 var(--s1);
    font-size: var(--t-micro);
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--muted);
}

.form-dialog .dual .search {
    width: 100%;
    margin: 0 0 var(--s1);
    padding: var(--s1) var(--s2);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
}

/* flex: 1 rather than a fixed height, so the two selects fill whatever the taller column set. */
.form-dialog .dual select {
    flex: 1;
    width: 100%;
    min-height: 14rem;
    padding: 2px;
    border: 1px solid var(--border-strong);
    border-radius: var(--r-md);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
}

/* Not italic, which is the browser default and reads as an aside. A category is a heading. */
.form-dialog .dual optgroup {
    font-style: normal;
    font-weight: 600;
    color: var(--muted);
}

/* The side padding is HALF the usual step, and it is load-bearing rather than cosmetic: every 4px
   here is 8px of text room per list, which is a whole 1rem step of dialog width bought for nothing.
   It is what keeps the longest live name uncut on a window that is merely wide rather than
   maximised — at a 1100px viewport the panel is capped below its 70rem and this is the margin. */
.form-dialog .dual option {
    padding: 2px var(--s1);
    color: var(--ink);
}

/* The Custom marker is in the option's TEXT (" · custom") and this only seconds it: a native list
   widget is drawn by the operating system and will not promise to honour a colour. */
.form-dialog .dual option.custom {
    color: var(--scope-custom, var(--ink));
}

.form-dialog .dual .btns {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: var(--s2);
}

.form-dialog .dual .btns .move {
    padding: var(--s1) var(--s3);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    line-height: 1;
    cursor: pointer;
}

.form-dialog .dual .btns .move:hover {
    border-color: var(--teal);
    color: var(--teal);
}

/* THE ARROWS SAY WHICH WAY THIS LIST IS LAID OUT, so both glyphs are in the markup and CSS shows
   one of them. Side by side, a move is left-to-right; stacked, it is downward, and an arrow still
   pointing sideways would be pointing at the dialog's edge. Nothing about the buttons themselves
   changes — same name, same value, same post — because the glyph is decoration and the button's
   accessible name is the visually-hidden label beside it. */
.form-dialog .dual .btns .move .down {
    display: none;
}

/* Below 900px the wide dialog is capped to the viewport and the two lists can no longer both be
   read, so the three columns stack — Available above Granted, each keeping its own search box —
   and the buttons run across instead of down (SAAS-134; it was 40rem before the dual list had a
   wide dialog to sit in). */
@media (max-width: 56.25rem) {
    .form-dialog .dual {
        grid-template-columns: 1fr;
        margin-left: 0;
    }

    .form-dialog .dual .btns {
        flex-direction: row;
        justify-content: center;
    }

    .form-dialog .dual .btns .move .across {
        display: none;
    }

    .form-dialog .dual .btns .move .down {
        display: inline;
    }
}

/* Held at the width the variant exists for, so a later change to the dialog's padding cannot
   quietly take it back. 72rem rather than the stacking breakpoint: this is where the panel stops
   being capped by the viewport and reaches its full 70rem, so the floor is met with room to spare
   and can never be the thing that starts a horizontal scroll. Between the two the lists are still
   side by side and simply narrower. */
@media (min-width: 72rem) {
    .form-dialog.wide .dual .col {
        min-width: 22.5rem;
    }
}

/* =============================================================================
   A DOCTOR'S MERGED LISTS (SAAS-146)

   Ruled by Darius on the SAAS-138 acceptance walk, 2026-08-25: a marked account reads ONE
   date-sorted list across the clinics it works at, where an Owner reads a block per clinic. Two
   things on screen are new, and both exist because the block frame is gone.
   ============================================================================= */

/* THE CLINIC, first column, on every merged row. It leads because it is what the reader most needs
   and least expects — a Billing table has never held two clinics' bills before, and a receipt
   number means nothing without it. Set in strong ink rather than muted: on this table it is not
   decoration but the half of the row's identity that makes the number readable at all. */
table.ledger th.clinic,
table.ledger td.clinic {
    white-space: nowrap;
}

table.ledger td.clinic {
    color: var(--ink-strong);
    font-weight: 600;
}

/* WHAT EACH CLINIC CONTRIBUTED, beside the list. The merged calendar's own list of sites, on the
   two list Surfaces — same shape, same weights, because it answers the same question: a clinic that
   contributed nothing is otherwise indistinguishable from a quiet year. */
.merged-sites {
    margin: var(--s4) 0 0;
    padding: 0;
    list-style: none;
    border-top: 1px solid var(--line);
}

.merged-sites li {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s1) var(--s3);
    align-items: baseline;
    padding: var(--s2) 0;
    border-bottom: 1px solid var(--line);
    font-size: var(--t-small);
}

.merged-site-name {
    min-width: 14rem;
    color: var(--ink-strong);
    font-weight: 600;
}

.merged-site-count {
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

/* A stated condition — "no column in your name", "you are not recorded as a doctor here", or the
   reach. Ordinary ink rather than a warning's: none of them is a fault, and the reader's next move
   differs for each. */
.merged-site-said {
    flex: 1 1 20rem;
    color: var(--ink);
}

/* ---------------------------------------------------------------------------
   THE DEPOSITS VIEW (SAAS-157, ADR 0026) - the Billing Surface's third view.

   Held is a LIABILITY rather than a takings figure, so it is drawn as a stated band rather than in
   the money type the ledger columns use: it is the frame the movements below are read inside, and a
   reader must not mistake it for a total of the rows under it. The rows themselves are the ledger
   table every other block on this Surface uses, unchanged. */
.held-headline {
    margin: 0 0 var(--s4);
    padding: var(--s3) var(--s4);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    background: var(--row-hover);
}

.held-headline.empty {
    background: transparent;
}

.held-headline p {
    margin: 0;
}

.held-figure {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s1) var(--s2);
}

.held-label {
    font-size: var(--t-small);
    font-weight: 600;
    letter-spacing: .01em;
    color: var(--muted);
}

.held-amount {
    font-size: var(--t-lead);
    font-weight: 700;
    color: var(--ink-strong);
    font-variant-numeric: tabular-nums;
}

/* "as of now" is said beside the figure and never below it. It is Outstanding's idiom, and the
   reason it earns space is that the figure moves under the reader: rerun this page in an hour and
   it is a different number. */
.held-when {
    font-size: var(--t-small);
    color: var(--muted);
}

.held-legs {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s1) var(--s4);
    margin-top: var(--s2) !important;
    font-size: var(--t-small);
    color: var(--muted);
}

.held-legs .leg strong {
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}

.held-nothing {
    font-size: var(--t-body);
    color: var(--muted);
}

.held-incomplete {
    margin-top: var(--s2) !important;
}

/* The movement kind. A word, never a colour alone - the three are told apart by reading them, and
   the wash exists so a reader scanning a column can find the unrecognised one rather than so the
   kinds are decoded from hue. */
.ledger td.movement .movement-kind {
    font-size: var(--t-small);
    font-weight: 600;
    color: var(--ink);
}

.ledger td.movement .movement-kind.unrecognised {
    padding: 0 var(--s1);
    border-radius: var(--r-sm);
    background: color-mix(in srgb, var(--ink) 9%, transparent);
    font-weight: 700;
}

/* Reading one patient (ADR 0026 ruling 4). It replaces the branch picker rather than sitting beside
   it, because while a patient is being read there is exactly one branch that identity means anything
   at - so there is nothing to pick. */
.branch-form .one-patient {
    margin: 0 0 var(--s4);
    font-size: var(--t-body);
    color: var(--ink);
}

.branch-form .one-patient a.clear {
    margin-left: var(--s2);
}

/* ================================================================================================
   THE 44px FLOOR (SAAS-163)
   ================================================================================================

   Touch targets, only where there is a touch. The portal is read at a desk most of the time and the
   report form is deliberately dense, so widening every control everywhere would cost the density for
   nothing. On a coarse pointer the 44px floor applies.

   WHAT WAS WRONG. The rule named `button` and `[type="submit"]` and nothing else, and the SAAS-128
   phase-1 measurement is what found the hole: the masthead's own destinations measured 37px, because
   they are ANCHORS and the rule reached buttons. SAAS-162 floored the two navigation targets it
   shipped, on the targets themselves. What was left was every other control in the portal — the
   pager, the calendar's Previous/Today/Next, the sort headings, the row handles into one patient and
   one receipt, the account menu, the operator screens' action links. This block is that inventory.

   LAST IN THE SHEET, ON PURPOSE. A floor that loses to the rule it is flooring is not a floor. Half
   of these selectors also exist above — `.pager .page` is drawn there, `.quick-ranges a.quick` is
   drawn there — and at equal specificity the later rule wins. Coming last is what lets this block
   name a control the way the rest of the sheet already names it, instead of inventing a heavier
   selector for each one and drifting from the rule it means to raise.

   THE SHAPE IS TWO RULES, and the split is the interesting part.

   1. THE FLOOR is declared once, on the elements rather than on a list of classes: `a`, `button`,
      `summary`, `[type="submit"]`. It reaches every control this portal will ever add without
      anybody having to remember to add it to a list.

      `min-height` DOES NOTHING TO AN INLINE BOX, and that is why the floor can be this broad. A link
      inside a sentence stays exactly the size of its words — which is what the target-size rules
      themselves ask for, since a 44px word mid-paragraph would break the line it sits in and buy
      nobody a bigger target. So the floor lands on precisely the links that are already boxes, and
      passes over prose.

   2. THE BOXES are the second rule: the standalone controls that are inline TODAY and would slip
      under a floor they deserve. Each is a link that is the whole of its cell, its paragraph or its
      strip — a breadcrumb, a row handle, a calendar step — so making it `inline-flex` costs no
      layout and lets rule 1 reach it.

   WHERE THE LINE IS. Navigation and controls are floored: links, buttons and disclosures. FORM
   FIELDS ARE NOT, and that is a boundary rather than an omission. Inputs, selects and their labels
   keep the density the report form was designed at, and the one concession this block already made
   to touch — a 1.15rem checkbox — is the ruling that stands. A 44px floor on filter labels would
   treble the height of an eleven-Branch picker, which is a density decision and not this one's.

   PINNED BY `TapTargetFloorTests`, which walks the components the app ships, collects every class
   that lands on a link, and fails when one turns up that neither this block nor a written exemption
   accounts for. The next control added under 44px fails a test rather than a walk. */
@media (pointer: coarse) {
    /* 1. THE FLOOR. */
    a,
    button,
    summary,
    [type="submit"] {
        min-height: 44px;
    }

    /* 2. THE BOXES: standalone controls that are inline until this rule, named by the container that
       makes each one standalone rather than by the page it turns up on.

       `.cal-more` and `.mw-more` are the two that cost something. They are the "+3 more" counters
       inside a calendar lane, positioned at a computed pixel offset, and they are also the ONLY way
       from a week to the day that holds the bookings they count — no day heading is a link. A 44px
       counter covers a little more of the lane's right edge than a 20px one does, and that is the
       price of the count being reachable at all on a phone. */
    .crumb a,
    .drill-down a,
    .sort-choice a,
    .block-link a,
    .record-list h2 a,
    .cal-nav a,
    .cal-branches a,
    .cal-more,
    .mw-more,
    .quick-ranges a.quick,
    td > a,
    dd > a,
    a.row-link,
    a.clear,
    a.owners-link,
    a.rotate-link,
    a.mint-link,
    a.claim-link,
    a.release-link,
    .export-link {
        display: inline-flex;
        align-items: center;
    }

    /* THE PAGER IS THE ONE STRIP THE ELEMENT FLOOR CANNOT CARRY, and it is worth saying why. Half of
       it is not links: the page you are on is a `<span>` and so is a step you cannot take, because
       neither goes anywhere (SAAS-68). A floor that only reached the anchors would leave the current
       number 21px tall between two 44px neighbours — which is worse than the defect it fixed. So
       the strip is floored by class, and the numbers are centred in a box now taller than a digit. */
    .pager .page,
    .pager .step {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-height: 44px;
    }

    /* The menu's rows span the popover, so this one is a full-width box rather than one shrunk to
       its own text — the row is the target, not the words. */
    .menu-item {
        display: flex;
        align-items: center;
    }

    /* A GLYPH IS NOT A WORD, so these three owe a width as well as a height: a close cross measured
       17px across, and a floor that only raised it would leave a 44px-tall sliver. Every other
       target on this list is a caption and is wider than 44px before anything here touches it. */
    .form-dialog .dialog-close,
    .report-list li .st,
    .form-dialog .dual .btns .move {
        min-width: 44px;
    }

    /* Centred in boxes now taller and wider than the labels they were drawn around. `text-align`
       says nothing inside a flex box, and a baseline leaves the text at the top of a 44px one. */
    .cal-more,
    .mw-more,
    .form-dialog .dialog-close {
        align-items: center;
        justify-content: center;
    }

    .brand,
    .report-list li .nm {
        align-items: center;
    }

    input[type="checkbox"] {
        width: 1.15rem;
        height: 1.15rem;
    }
}


/* -------------------------------------------------------------------------
   THE BILLING SURFACE, MERGED (SAAS-172)

   The per-Branch block retired from Billing, so two hooks the old shape did
   not need arrive with it. Written in ASCII on purpose: this file already
   carries some double-encoded punctuation from an earlier slice, and an
   editor that re-saved the whole thing would spread it.
   ------------------------------------------------------------------------- */

/* The Branch filter, now one field among the others rather than a picker above
   them. It is a fieldset, so it needs the field's own spacing without the
   browser's default legend inset. */
.ledger-filters .field-branches {
    border: 0;
    margin: 0;
    padding: 0;
    min-width: 0;
}

.ledger-filters .field-branches > legend {
    padding: 0;
}

/* HELD DEPOSITS, ONE PER CONTRIBUTING CLINIC, ABOVE THE MERGED TABLE.
   Stacked rather than laid side by side: these are separate liabilities that
   must not read as columns of one figure, and a clinic's name sits on each. */
.held-per-branch {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}
