/* ======================================================================
   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;

    /* Reserve the scrollbar's width, so a page that grows past the fold does not shift sideways. */
    scrollbar-gutter: stable;

    --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);

    /* THE INK A NAME THAT OPENS A THING IS WRITTEN IN (SAAS-363) — --nav-active-ink's own move,
       for --nav-active-ink's own reason, one surface along: the deep end of the teal in light and
       the lifted end in dark, which is what `var(--teal-deep)` already means in each theme.

       ⚠ IT IS NOT --action, AND THAT IS MEASURED RATHER THAN CHOSEN. These are words, so the floor
       is 4.5:1 and not the 3:1 a non-text mark may take. Every table that draws one of these links
       sits on --page and washes its rows, and --action fails on four of the five backdrops that
       actually occur in the light theme — 4.65:1 on --page, then 4.34:1 on a hovered row, 4.39:1
       down the register's sorted column, 4.38:1 on an unmeasured Branch's row and 4.17:1 when that
       row is hovered. The Branch name has been drawing at those figures since SAAS-51; nothing had
       measured it against the text floor. --teal-deep clears every one of them: 6.74 / 6.29 / 6.35
       / 6.35 / 6.05 in light, 11.13 / 10.35 / 10.61 / 10.55 / 9.99 in dark. */
    --row-link: 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: six, and no more.

       ⚑ SIX HUES, THEN NONE — SAAS-255, ruled 2026-09-07 from a real eleven-Branch Owner's week.
       The palette USED TO REPEAT past six, and this comment used to say so. At eleven clinics that
       put five pairs of Branches in one colour, and the reader could no longer trust that two
       different colours meant two different clinics. It does not repeat any more: the first six
       Branches in Branch order keep a hue, and from the SEVENTH ON a Branch has none — its blocks,
       its legend entry and its agenda rows are drawn in ink (--muted, .tone-0 below) with the code
       alone. Twelve measured hues were rendered and rejected: every one cleared 4.5:1, but four of
       them were blues a reader has to tell apart at 11px bold, and a colour that only pretends to
       separate is worse than a grey that says it is not trying. ADR 0020's exception therefore
       stays exactly as narrow as SAAS-151 made it — six Branch hues past the action and the status
       ladder, and nothing more.

       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 seventh Branch's ink is
       --muted, which measures 4.97:1 on light --surface and 4.68:1 on light --page, and 7.03:1 and
       7.65:1 on dark's two. The floor that matters is 4.5:1, because the code is 11px text and
       11px bold is not "large"; light is the constrained theme and it clears it on both grounds.
       The swatch and the block's own edge clear the 3:1 non-text floor several times over as a
       consequence.

       ⚠ THE LETTERS ARE THE TENANT'S, not ours (SAAS-255 ruling 1C). The code printed here is the
       SHORT CODE the Tenant named for the clinic — BDG, BGR, BKS — and only derived from the
       display names where nobody has named one. It is never the "Branch code" the New Branch form
       asks for: that one is the database code, `malabar21`, and no database name is ever drawn. */
    --branch-1: #2E5AAC;
    --branch-2: #7A3E9D;
    --branch-3: #A83A6B;
    --branch-4: #3E5578;
    --branch-5: #5F4A78;
    --branch-6: #7A4A60;

    /* ------------------------------------------------------------------
       THE CHART PALETTE (SAAS-266, ADR 0020's second addendum, built on SAAS-315).

       This is the THIRD use of colour this sheet's own rule admits, after the one action and the
       status ladder, and it is SAAS-151's shape: five categorical slots, then ink with a texture,
       plus a two-step ramp of one hue for every two-series chart. It was COMPUTED, not picked —
       docs/research/dashboard-chart-method.md carries the validator output for both themes.

       WHY THE SIX BRANCH HUES COULD NOT BE REUSED. They were made to be legible as 11px LABELS on
       two surfaces, not to be distinguishable as ADJACENT FILLS, and they fail hard as the latter:
       #5F4A78 against #3E5578 measures deltaE 0.7 under deuteranopia and 5.7 under full colour
       vision, on a pair the merged calendar shows side by side. Two Branch blocks are separated by
       a short code and never touch; two donut arcs touch and carry nothing but colour.

       WHY THE FIFTH SLOT IS A LIGHTNESS STEP RATHER THAN A FIFTH HUE. Reserving teal (the action
       and the chrome), green/amber/red (the ladder) and --star's burnt orange leaves a single 151
       degree arc free: teal 205 -> blue -> violet -> magenta 356. Four hue families, five slots.
       The order that passes the validator in BOTH themes is teal, violet, magenta-light, blue,
       magenta-deep, and the ring's wrap pair (slot 5 touching slot 1) was measured by hand:
       deltaE 20.9 CVD in light, 13.7 in dark.

       ⚠ EVERY CHART SITS ON --surface, NEVER ON --page. On light --page #F7F8FA slot 1 falls to
       2.84:1 and slot 3 to 2.88:1, both under the 3:1 mark floor, where on --surface they are 3.01
       and 3.06. The headroom is one hundredth of a ratio point, which is the same thin margin the
       teal masthead already recorded: any future tint of either surface has to be RE-MEASURED
       rather than eyeballed. The .block wrapper on every panel is what keeps the promise.

       EVERY SLOT ON --surface, BOTH THEMES, against the 3:1 mark floor. This is the bar SAAS-356
       did NOT waive, and ChartYearInkTests derives these figures off this file, so a hex edited
       above without editing this table fails the suite.

       | on --surface | light | dark  | bar  |
       | ------------ | ----- | ----- | ---- |
       | --chart-1    |  3.01 |  6.01 | 3.00 |
       | --chart-2    |  8.10 |  3.02 | 3.00 |
       | --chart-3    |  3.06 |  5.28 | 3.00 |
       | --chart-4    |  5.43 |  5.29 | 3.00 |
       | --chart-5    |  8.52 |  3.01 | 3.00 |

       ⚠ AT MOST TWO SERIES WHERE ANY MARK MAY TOUCH ANY OTHER. Under --pairs all the five-slot set
       fails (deltaE 5.9 deutan on #D37798 against #0BA5A9) and so does any three of them; two
       pass. A future scatter of clinics, or a choropleth of Jakarta, has to fold to two series,
       facet, or change form. Re-ordering cannot fix it.

       ⚑ AND THERE IS NOW ONE ACCEPTED EXCEPTION TO THAT BOUND, RULED BY DARIUS ON 2026-09-18
       (SAAS-356), IN HIS WORDS: "just give color, i dont care about color blind for now". He had
       just read SAAS-336's chart and said "336 i see only greys, i want colors" — accurately, since
       it drew one teal and every prior year the same grey.

       So the MULTI-YEAR COMPARISON takes one of these slots per year, newest on slot 1, and draws up
       to five lines that may cross anywhere. On that one panel the colour-blind bar above is WAIVED.
       Nothing else waives it, and it is waived "for now" — his words, kept because they are the
       scope of the decision.

       ⚑ AND SINCE 2026-09-20 THE COLOUR IS NO LONGER THE WHOLE SEPARATION: the year in hand is the
       only SOLID line and every prior year is DASHED, which is a channel no waiver touches. See
       bar 3 below for the ruling and for what the dash does and does not do.

       ⚠ THREE BARS LIVE IN THIS BLOCK AND THE RULING MOVES EXACTLY ONE. They are three different
       measurements and a reader who folds them together will either re-break the chart or refuse a
       change Darius has already made.

         1. CONTRAST AGAINST THE GROUND — NOT WAIVED. Every slot clears the 3:1 mark floor on
            --surface in both themes; the table two paragraphs up is the measurement, and
            ChartYearInkTests asserts it off this file, so a hex edited here without re-measuring
            fails the suite.

         2. COLOUR-BLIND SEPARATION BETWEEN MARKS — WAIVED, 2026-09-18, for the comparison only.
            Measured the same day under --pairs all over the five slots (OKLab, Machado 2009 at
            severity 1.0), worst pair in each model:

            | worst all-pairs deltaE | light |            | dark  |            |
            | ---------------------- | ----- | ---------- | ----- | ---------- |
            | protanopia             |   6.4 | 1 v 3      |  10.8 | 1 v 3      |
            | deuteranopia           |   5.9 | 1 v 3      |   5.2 | 1 v 3      |
            | tritanopia             |  13.9 | 1 v 4      |   4.2 | 1 v 4      |

            A reader with deuteranopia cannot separate three or more year lines by hue, and in dark
            a reader with tritanopia cannot separate slots 1 and 4 by hue. Darius has accepted that
            trade. It is written here so it can be REVISITED rather than found by surprise: it is an
            accepted exception to the accessibility bar ADR 0020 set and SAAS-266/336 re-derived,
            not a repeal of it.

            ⚠ THE 2026-09-20 DASH NARROWS THIS WAIVER AND DOES NOT LIFT IT. The year in hand is now
            solid against dashed prior years, so that one pair is separable with no colour vision at
            all; two PRIOR years are not, and this waiver is what covers them. The figures above are
            the colour channel's and are left exactly as measured.

         3. NORMAL-VISION SEPARATION — ⚠ UNDER THE FLOOR ON COLOUR ALONE, AND RULED ON 2026-09-20
            (SAAS-356): THE PRIOR YEAR CARRIES A NON-COLOUR CHANNEL AS WELL AS ITS HUE.
            The validator's floor is 15. The five slots measure 12.2 in light (slot 2 #4F4591
            against slot 4 #0F6AC3), and in dark 12.6 (slot 1 against slot 4) and 13.7 (slot 2
            against slot 5; recomputed in OKLab 2026-10-02, unrecorded until then).

            ⚠ THOSE TWO FIGURES AND THAT FLOOR STAY WRITTEN HERE. They are not deleted now that the
            reading no longer rests on colour alone. The next person to touch these hues has to know
            that the colour channel is under the floor, and why that is acceptable.

            Darius accepted the five hues on 2026-09-19 after seeing them drawn — "356 color is ok"
            — and then ruled on 2026-09-20 that colour may not carry the distinction by itself:
            THE YEAR IN HAND DRAWS SOLID AND EVERY PRIOR YEAR DRAWS DASHED. The pattern is
            ChartScale.PriorYearDash, and a dash is GEOMETRY RATHER THAN INK — so it survives both
            themes and a monochrome printer, which is where colour is least reliable. It answers the
            dark 12.6, whose slot 1 is the year in hand. It does NOT answer the light 12.2 or the
            dark 13.7: both are two PRIOR years, dashed alike, and they rest on the 2026-09-19
            acceptance of the hues alone, and Darius ruled on 2026-10-02 that prior years need NOT
            be told apart without colour (SAAS-356). Moving a hue would have cleared the 15, left the
            colour-blind concession exactly where it was, and risked the brand teal.

            ⚠ AND WHAT THE DASH DOES NOT DO, so nobody reads more into it than was ruled. It
            separates the year in hand from the prior years. It does NOT separate two prior years
            from each other: a dash ramp per year is the encoding the 2026-09-18 ruling refused, so
            three or more prior years are still told apart by hue, by vertical position and by the
            year each line carries at its own right-hand end. The CONVERGENCE BAND is where that
            costs something — on a five-year reading four of the five lines sit between roughly 500m
            and 620m across July and August, which is the case to look at first if this is ever
            revisited.

       ⚑ AN UNEXPLAINED GAP IN THIS BLOCK IS EXACTLY HOW THE MISTAKE SAAS-336 WAS RAISED OVER GOT
       IN — the old guard measured each line against the ground, the comment said nothing about
       neighbours, and four indistinguishable greys shipped under a green suite. So a waived bar is
       written down as waived. A later reader must see a decision, never an omission.

       Slot 1 is a teal and measures deltaE 12.2 from --action in light. Accepted knowingly: a fill
       and a button never touch as marks. */
    --chart-1: #0BA5A9;
    --chart-2: #4F4591;
    --chart-3: #D37798;
    --chart-4: #0F6AC3;
    --chart-5: #911952;

    /* Everything outside the Tenant's five Category Palette slots, and the Other staff bar. It is
       --muted, deliberately, so it cannot be mistaken for a sixth hue — and it carries a 45 degree
       hatch as well, because a grey among five hues is told apart only BY its greyness. */
    --chart-ink: #667085;

    /* The two-step ordinal ramp: Existing against New, female against male. One hue at two
       lightnesses, and the gap between them is measured rather than judged (SAAS-324).

       WHY IT MOVED. The ramp shipped as #006B74 / #00B3C2 here and #00818C / #00C1D1 in dark,
       which measure 2.45:1 and 2.11:1 against each other — both under the 3:1 floor WCAG 1.4.11
       sets for two marks that touch, and dark was the worse of the two. Darius read Puri's
       295.1 M bar on the live dashboard as one teal block with a barely distinguishable tip. The
       target is the example's own pair, #1F8FE6 light blue on #1B2A8C navy at 3.49:1, because
       clearing 3:1 by a whisker would still look like the thing he complained about.

       STILL ONE HUE. Every step below is hsl(184.65 …) — the ramp's own hue, unchanged. The pair
       moved apart in lightness and in nothing else, so ADR 0020's one-hue rule holds and no
       second categorical slot was spent on a distinction that is not categorical.

       | measured 2026-09-12             | light | dark  | bar  |
       | ------------------------------- | ----- | ----- | ---- |
       | deep against light              |  3.65 |  3.62 | 3.00 |
       | deep on --surface               | 12.86 |  3.56 | 3.00 |
       | deep on --page                  | 12.10 |  3.87 | 3.00 |
       | light on --surface              |  3.53 | 12.88 | 3.00 |
       | light on --page                 |  3.32 | 14.00 | 3.00 |
       | --chart-step-deep-ink on deep   | 12.86 |  5.09 | 4.50 |
       | --chart-step-light-ink on light |  5.03 | 12.62 | 4.50 |

       ⚠ THE SURFACE BOUNDS THIS, and it bounds it from opposite sides in the two themes: in light
       both steps must stay dark enough to clear #FFFFFF and the #F7F8FA page behind it, and in
       dark both must stay light enough to clear #14161A. That pair of walls is what made the old
       ramp narrow, and it is why the dark deep step sits near its own floor — every unit of
       lightness it gains, the light step must gain 3.6 of. Any future tint of either surface
       re-measures all seven rows above; ChartRampContrastTests asserts every one of them off this
       file, so a hex edited here without re-measuring fails the suite. */
    --chart-step-deep: #00383D;
    --chart-step-light: #0097A3;

    /* THE INK A SEGMENT'S OWN LABEL IS DRAWN IN, one token per step (SAAS-325). A stacked segment
       prints its value inside itself, directly on that step's fill — the segment is the chip — so
       the ink has to clear that fill at 4.5:1, and one ink cannot clear both ends of a ramp that
       is 3.6:1 wide.

       ⚠ WHITE ON BOTH STEPS IS ARITHMETICALLY IMPOSSIBLE, which is why there are two tokens.
       In dark a mark must clear --surface #14161A at 3:1, which puts its relative luminance at
       0.124 or above; white clears a fill at 4.5:1 only at 0.183 or below; and a pair 3.49:1 apart
       cannot put both ends inside that band. The example is not a counter-example — white on its
       own #1F8FE6 measures 3.43:1, which is the large-text bar and not this one.

       Both tokens are theme-invariant, so the dark block redeclares neither: the deep step is dark
       in both themes and the light step is light in both. A near-black label on a pale cyan fill
       in the dark theme is the deliberate consequence. */
    --chart-step-deep-ink: #FFFFFF;
    --chart-step-light-ink: #101828;

    /* ⚑ --chart-year-now AND --chart-year-prior ARE RETIRED (SAAS-356, 2026-09-18). Retired rather
       than redefined onto the slots, which the ruling offered as the alternative: there is no longer
       a NOW colour and a PRIOR colour to name. A year is a slot, so the slot tokens above are the
       whole statement and a second name for slot 1 would be one more place it could drift. SAAS-339
       set the precedent for retiring a chart token rather than leaving it pointing somewhere.

       WHAT THEY WERE, so the retirement reads as a decision. SAAS-328 ruling 5 treated years as
       ORDINAL and therefore not as palette slots: the year in hand in --teal (#0E7C86 light,
       #4FC3CE dark) and every prior year in --chart-ink (#667085 / #98A2B3). SAAS-336 amended it
       from a four-step grey ramp to one grey plus a dash per year, having measured that the four
       greys ran 1.12–1.16:1 against EACH OTHER inside a usable band 1.54:1 wide — one 3:1 step does
       not fit in that, let alone three.

       WHY THE ORDINAL READING LOST. It was sound and it was unreadable. Darius, 2026-09-18: "336 i
       see only greys, i want colors", then "just give color, i dont care about color blind for now".
       A four-year comparison drew three grey lines and one teal, which is what he was looking at. So
       a year is categorical now — the thing ruling 5 said it was not — and the dash patterns went
       with the grey they were compensating for (ChartScale.YearStroke carries the rest).

       ⚠ THE COLOUR-BLIND BAR THIS COSTS IS WAIVED AND WRITTEN DOWN, in the --chart-1 block above,
       with the measurement and the date. Two of the three bars there still hold. */

    /* A gridline, the target mark, and the unfilled part of a gauge. The target is --ink because it
       is a REFERENCE rather than a status: a dashed line in amber would read as a warning. */
    --chart-grid: #E4E7EC;
    --chart-target: #101828;
    --chart-track: #EEF0F3;

    /* 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;

        /* The chart palette, lifted (SAAS-266). Slot 1 is unchanged: #0BA5A9 measures 3.01:1 on
           light --surface and 6.01:1 on dark's, so one teal serves both themes and the brand's
           presence on the dashboard does not move between them. The other four are the dark
           theme's own values, and all five clear 3:1 on --surface here. */
        --chart-1: #0BA5A9;
        --chart-2: #6353BD;
        --chart-3: #DB6190;
        --chart-4: #1F8FE6;
        --chart-5: #924B77;
        --chart-ink: #98A2B3;

        /* The ordinal ramp, widened with the light theme's (SAAS-324) — same hue, 3.62:1 apart.
           The deep step sits near its own floor at 3.56:1 on --surface, deliberately: every unit
           of lightness it gains the light step has to gain 3.6 of, and the light step is already
           pale. The two label inks are declared once in :root and hold here unchanged; the
           measured table for both themes is beside them. */
        --chart-step-deep: #007A85;
        --chart-step-light: #7CEAF4;

        /* The two ordinal YEAR tokens are retired here too (SAAS-356) — see :root for why. This
           theme declared them at #4FC3CE and #98A2B3; a year now wears one of the five slots above,
           which this theme already declares its own values for. Dark had MORE room than light for
           the grey ramp and it made no difference, so nothing about the retirement is theme
           specific. */

        --chart-grid: #23262B;
        --chart-target: #ECEFF3;
        --chart-track: #1F2227;

        /* 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 INFORMATION MARK (SAAS-462, ADR 0020 addendum of 2026-10-02).

   Darius, 2026-10-02, with a screenshot of /portal and its five-line paragraph: "we have to many
   text that explain things to the user, and it takes too much space, consider hiding them under a
   mouseover icon beside the thing that they explain". The component is InfoMark.razor, which holds
   the rule about WHAT goes behind it (an explanation) and what never does (state: a warning, a
   refusal, an error, an empty-state sentence, a "synced at" fact, a confirmation, a field's format
   rule). This is how it is drawn and how it opens.

   NO SCRIPT TO OPEN IT. Hover and keyboard focus are `:hover` and `:focus-within`, and a tap
   focuses the button on every browser that focuses a button on tap, so the panel opens there too and
   a tap elsewhere closes it. The one thing CSS cannot do is a tap on Safari, which does not focus a
   button — nav.js sets `data-open` for that, and adds Escape (`data-dismissed`) and click-away.

   THE PANEL IS ANCHORED TO THE MARK and is at most 40ch wide. On a phone the mark can sit anywhere
   along a line, so a panel hung off its left edge would run past the viewport: below 768px the mark
   stops being the positioning box, and the panel takes its horizontal extent from the nearest
   positioned ancestor (the page, when there is none) with the page's own gutters, while its
   vertical place stays where the markup put it — directly under the mark. Never wider than the
   viewport at 375px, by construction rather than by a measured luck.

   DESIGN TOKENS ONLY (ADR 0020): the panel is the popover idiom the account menu already is
   (`--surface`, `--border`, `--e2`), the glyph is `--muted` and takes `--ink-strong` when it is the
   thing that is open. No colour is spent on it, which is the whole of the rule.

   PRINT DROPS THE MARK AND ITS TEXT, in the print block at the foot of this file: paper reads like
   the screen, and the screen no longer carries the paragraph.
   ============================================================================================== */
.info-mark {
    position: relative;
    display: inline-block;
    vertical-align: middle;
}

/* A heading or a label followed by a mark sits on the same line as it. The mark is a SIBLING, never
   inside the heading (see InfoMark.razor: inside, its button name would become part of the
   heading's own), so the line is made by the heading becoming inline-block. `:has()` keeps every
   heading that has no mark exactly as it was. */
h1:has(+ .info-mark),
h2:has(+ .info-mark),
h3:has(+ .info-mark),
h4:has(+ .info-mark),
p:has(+ .info-mark) {
    display: inline-block;
    vertical-align: middle;

    /* ROOM LEFT FOR THE MARK (SAAS-478). An inline-block is as wide as its words, up to the line, so
       a long title or a three-line sentence took the whole line and the mark fell to the next one,
       alone (an Operator's People title and a patient's record title at 375 px). The words give up
       the mark's own width — `--s7`, which is the 44 px touch button and its 4 px margin — so the
       mark always has a place beside them. */
    max-width: calc(100% - var(--s7));
}

/* A field's label, legend or caption and the mark that explains it, on one line, on every form
   (SAAS-473 part b) — the filter strip's own `.field-head`, which part a scoped to its strip, made
   general: the hints of the write forms (an Operator's editors, a Branch's settings) moved behind
   marks beside their labels too. The strip's rule is more specific and wins there, unchanged. */
.field-head {
    display: flex;
    align-items: center;
    gap: 0;
}

/* =======================================================================================
   A MARK SITS BESIDE THE WORDS IT EXPLAINS, ON EVERY SCREEN (SAAS-478)

   SAAS-473 part b put the remaining explanations behind marks, and a mark landed ALONE on the line
   under the words it explains, with empty space around it, in two places named at the time: after a
   legend and after a heading that holds a picker. A census in a browser over every screen at 375
   and 1440 px found three more shapes: a sentence a dialog's own rule had already made a block, a
   title too long to leave the mark room, and a title wrapped in a head block of its own.

   ONE PATTERN, WRITTEN ONCE AND KEYED ON THE SHAPE RATHER THAN ON THE SCREEN: the mark is the
   SIBLING right after its words (never inside them: a button inside a heading or a legend becomes
   part of that heading's or group's name), and a `:has(+ .info-mark)` rule on the words puts the
   two on one line. The heading's rule above is the first of them, and gives the mark room; the
   rules below are the others. No screen places a mark by hand.
   ======================================================================================= */

/* A sentence a dialog or a form had already made a block (`.form-dialog .hint` is
   `display: block`, which is one class more specific than the rule above) — the state sentence
   whose reason is behind the mark. The `.hint` in the selector is what out-ranks it. */
p.hint:has(+ .info-mark) {
    display: inline-block;
    vertical-align: middle;
    max-width: calc(100% - var(--s7));
}

/* A heading that is a picker: the Branch's Targets heading is a <details> whose summary holds the
   <h2> (SAAS-357), and a <details> is a block, so the mark after it drew on the next line. As an
   inline-block it is one box on the heading's line. Its panel hangs off the disclosure
   (`position: relative`), which is as wide as the heading and is all the panel's anchor needs. */
details.pick-year:has(+ .info-mark) {
    display: inline-block;
    vertical-align: middle;
    max-width: calc(100% - var(--s7));
}

/* A FIELDSET'S LEGEND AND ITS MARK, ON ONE LINE, WITH THE LEGEND'S NAME UNCHANGED. The mark stays
   OUTSIDE the <legend>, so the group is still named by the legend's own words and nothing else —
   which is the whole reason it was a sibling and therefore on its own line.

   A fieldset's *rendered* legend is laid out in the fieldset's border and is no part of any flex
   or grid the fieldset becomes (measured in Edge: both left the mark on the next line). A FLOATED
   legend is an ordinary box, and Bootstrap's reboot has floated legends for years for exactly
   that reason; the accessible name of a fieldset is its first child `legend` whatever that
   legend's `float`, so nothing a screen reader says changes. What a sighted reader sees change
   is that a BORDERED fieldset's legend sits inside the top edge instead of breaking it.

   Both are floats, and the legend is as tall as the mark's button (28 px, `--s5` and `--s1`), its
   words centred in that, so the pair is one row. The legend gives up the mark's width, so a long
   legend wraps instead of pushing the mark off the row. The first thing after the mark clears
   them, so a legend that wrapped to two lines still holds the content under it. */
fieldset > legend:has(+ .info-mark) {
    float: left;
    display: flex;
    align-items: center;
    box-sizing: border-box;
    min-height: calc(var(--s5) + var(--s1));
    max-width: calc(100% - var(--s7));
}

fieldset > legend:has(+ .info-mark) + .info-mark {
    float: left;
}

fieldset > legend:has(+ .info-mark) + .info-mark + * {
    clear: both;
}

.info-mark-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    min-width: 28px;
    min-height: 28px;
    margin: 0 0 0 var(--s1);
    padding: 0;
    background: none;
    border: 0;
    border-radius: 50%;
    color: var(--muted);
    cursor: help;
    vertical-align: middle;
}

.info-mark-button:hover,
.info-mark:focus-within .info-mark-button,
.info-mark[data-open] .info-mark-button {
    color: var(--ink-strong);
}

.info-mark-panel {
    display: none;
    position: absolute;
    z-index: 30;
    top: 100%;
    left: 0;
    width: max-content;
    max-width: min(40ch, calc(100vw - var(--s6)));
    /* A long key scrolls inside its own panel rather than running off the bottom of the screen. */
    max-height: min(70vh, 32rem);
    overflow-y: auto;
    box-sizing: border-box;
    margin: 0;
    padding: var(--s3);
    background: var(--surface);
    color: var(--ink);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    box-shadow: var(--e2);
    /* It is drawn inside headings' neighbours and labels, so it takes none of their type. */
    font-size: var(--t-small);
    font-weight: 400;
    line-height: 1.5;
    letter-spacing: 0;
    text-transform: none;
    text-align: start;
    white-space: normal;
}

/* The panel starts exactly where the mark ends — no gap — so the pointer's way from the button to the
   text it wants to read never crosses ground that is neither: a gap would close the panel the moment
   the pointer left the button. */
.info-mark:hover .info-mark-panel,
.info-mark:focus-within .info-mark-panel,
.info-mark[data-open] .info-mark-panel {
    display: block;
}

/* Escape closes it without taking the keyboard's place away from the button, and the next move of
   the pointer or the focus lets it open again. Written after the rules that open it, at the same
   specificity, so it wins. */
.info-mark[data-dismissed] .info-mark-panel {
    display: none;
}

/* What a panel holds: paragraphs, and a definition list when the explanation is a key. They take the
   panel's own type and spacing, not the page's. */
.info-mark-panel p {
    margin: 0;
    color: inherit;
    max-width: none;
}

.info-mark-panel p + p,
.info-mark-panel p + dl,
.info-mark-panel dl + p {
    margin-top: var(--s2);
}

.info-mark-panel dl {
    margin: 0;
}

.info-mark-panel dt {
    margin: var(--s2) 0 0;
    font-weight: 650;
    color: var(--ink-strong);
}

.info-mark-panel dt:first-child {
    margin-top: 0;
}

.info-mark-panel dd {
    margin: 0;
}

.info-mark-panel a {
    color: var(--action);
}

/* =================================================================================================
   THE SYNC MARK (SAAS-469, ADR 0031's eighth addendum): what a Branch's copy has to say about itself,
   behind an AMBER mark beside the page's title instead of a box above its content.

   Darius, 2026-10-02 about 23:35, with a phone screenshot where the stale notice pushed the schedule
   off the screen: "hide the sync message in mouse over tooltips too". The component is SyncMark.razor,
   which carries the words (unchanged) and the rule that narrows SAAS-462's "state is never hidden": by
   his word, for this message only, and what keeps it from being quiet is that the mark is amber and
   visible. THE MECHANICS ARE THE INFORMATION MARK'S — `.info-mark` is on the element, so hover, focus,
   tap, Escape, click-away and the phone panel that stays inside a 375px screen are those rules and
   none of them is copied here. What is below is only what is different: a pill with a dot, the line,
   and a tinted panel.

   AMBER IS --warn, THE PRODUCT'S TOKEN FOR A STATED CONDITION (ADR 0020), and it is the DOT and the
   edge that carry it. The words stay in ink: --warn on the page measures 4.32:1 against the wash a
   hovered row puts under it, which a 13px caption cannot spend. An UNMEASURED copy keeps its achromatic
   look — a Server with no Sync Reader recorded is Daclaria's own missing step and not a clinic in
   trouble, the same argument `p.stale-notice.unmeasured` already makes — so its dot is --muted.
   ============================================================================================== */
.info-mark.sync-mark {
    vertical-align: middle;
    /* A gap before whatever follows on the line — the Focus on switch sits right after the mark on the
       calendars, and the capture at 1440px showed the pill touching its label. */
    margin-inline-end: var(--s3);
}

.info-mark-button.sync-mark-button {
    gap: var(--s2);
    min-width: 0;
    min-height: 30px;
    margin: 0 0 0 var(--s2);
    padding: 0 var(--s3);
    border: 1px solid var(--border-strong);
    border-radius: 999px;
    background: var(--surface);
    color: var(--ink);
    font-size: var(--t-small);
    font-weight: 600;
    line-height: 1;
    cursor: help;
    white-space: nowrap;
}

.sync-mark[data-sync="warn"] .sync-mark-button {
    border-color: color-mix(in srgb, var(--warn) 45%, transparent);
    background: color-mix(in srgb, var(--warn) 8%, transparent);
}

.sync-mark-dot {
    flex: 0 0 auto;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--muted);
}

.sync-mark[data-sync="warn"] .sync-mark-dot {
    background: var(--warn);
}

.sync-mark[data-sync="unmeasured"] .sync-mark-dot {
    background: var(--muted);
}

.sync-mark-glyph {
    flex: 0 0 auto;
    color: var(--muted);
}

.sync-mark-button:hover .sync-mark-glyph,
.sync-mark:focus-within .sync-mark-glyph,
.sync-mark[data-open] .sync-mark-glyph {
    color: var(--ink-strong);
}

/* The panel wears the same tint as the pill, and holds the paragraphs the page used to draw as
   boxes. They are not boxes any more: the panel is the box. */
.sync-mark[data-sync="warn"] .sync-mark-panel {
    border-color: color-mix(in srgb, var(--warn) 45%, var(--border));
    background: color-mix(in srgb, var(--warn) 7%, var(--surface));
}

.sync-mark-panel {
    max-width: min(46ch, calc(100vw - var(--s6)));
}

.sync-mark-head {
    display: block;
    margin: 0 0 var(--s2);
    color: var(--ink-strong);
    font-weight: 650;
}

.sync-mark-panel p.stale-notice {
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    border-radius: 0;
    max-width: none;
    color: inherit;
}

.sync-mark-panel p.stale-notice + p.stale-notice {
    margin-top: var(--s2);
}


/* --- 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) — and the phone's shape is ONE CLOSED
   BAR since SAAS-463.

   ShellNav draws the areas and the current area's views ONCE, inside one <details class="shell-
   menu">. This sheet lays that one markup out twice:

     · 768px and up — DIRECTION A. 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. The <details> is made
       transparent to layout here, so the rail is exactly what SAAS-162 drew.
     · Below 768px — A CLOSED MENU BAR. A 44px Menu button on the masthead's own teal, naming the
       current area. Opened, the navigation is the SAME COLUMN the rail is, on the page's surface:
       areas, the current area's views nested under it, the Administrative group last.

   ⚑ THE PHONE'S SHAPE WAS DIRECTION B UNTIL 2026-10-02 and Darius reversed it himself ("mobile
   view need some consideration too, the menu now takes too much space on the top, we need to use
   collapsible menu"). B drew every area as a row on the masthead's colour and the views as a band
   beneath: 273px to 329px from the top of the viewport to the page (a 50px masthead and 223px to
   279px of navigation), measured at 375px on /portal, the
   dashboard, the merged calendar and Billing, before the page's title. ADR 0027's addendum of
   2026-10-02 records the reversal and its stated cost: the menu is closed on every page load, so
   the bar names the current area.

   The rules below are written narrow-first, so the phone shape is the base and the rail is what a
   media query adds. The area, view and group rules are the rail's and are shared by both widths —
   an open phone menu is that column — so what the two widths differ in is the CONTAINER only.

   NO SCRIPT FOR THE SHAPE. A <details> opens on click and on Enter and is in the tab order; nav.js
   adds Escape and click-away, as it does for the account menu. Nothing is stored or restored.
   ============================================================================================== */

/* 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;
}

/* THE MENU BAR (SAAS-463). One row, 44px — the floor every target here clears — wearing --brand so
   it reads as the masthead's own continuation: the header ends and this begins with no gap and no
   line between them. Full --on-brand, not a quieter tint, and that is measured rather than chosen:
   --brand is teal at 4.95:1 against white, so there is almost no headroom (88% white measures
   4.21:1, 92% measures 4.45:1, both under the 4.5:1 floor). */
.shell-menu {
    display: block;
}

.shell-menu > summary {
    display: flex;
    align-items: center;
    gap: var(--s3);
    box-sizing: border-box;
    min-height: 44px;
    padding: var(--s1) var(--s4);
    background: var(--brand);
    color: var(--on-brand);
    cursor: pointer;
    list-style: none;
}

/* The marker goes in both engines' spellings, or the browser draws its own triangle beside our
   glyphs — the account menu's own note. */
.shell-menu > summary::-webkit-details-marker {
    display: none;
}

/* The two washes are translucent black (see --masthead-hover), so they go OVER --brand as an image:
   put in `background-color` they would replace the teal with grey, which is how the first render
   of this bar looked. Cutting into the band rather than lifting out of it is also the measured way
   round — white on a lightened teal drops under 4.5:1, on a darkened one it rises. */
.shell-menu > summary:hover {
    background-image: linear-gradient(var(--masthead-hover), var(--masthead-hover));
}

/* Open is a shade deeper than closed, so the bar reads as the lid of the list under it. The state
   is also in the chevron, which turns, and in the list itself; colour is never the only carrier. */
.shell-menu[open] > summary {
    background-image: linear-gradient(var(--masthead-current), var(--masthead-current));
}

.shell-menu-icon {
    flex: 0 0 auto;
}

.shell-menu-label {
    flex: 0 0 auto;
    font-weight: 650;
}

/* Where you are, beside the button. It is the one thing the closed bar must say, because a menu
   that re-closes on every page cannot be read for it. It truncates rather than wraps: one row is
   the whole point of the bar. A rule down its left is the divider — currentColor at a fraction, so
   no new colour is spent. */
.shell-menu-here {
    min-width: 0;
    padding-inline-start: var(--s3);
    border-inline-start: 1px solid color-mix(in srgb, currentColor 45%, transparent);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.shell-menu-chevron {
    flex: 0 0 auto;
    margin-inline-start: auto;
    transition: transform 160ms ease;
}

.shell-menu[open] > summary .shell-menu-chevron {
    transform: rotate(180deg);
}

/* THE OPEN MENU: the rail's own column, on the page's surface. It follows the bar in the flow, so
   it pushes the page down rather than floating over it — nothing to dismiss, nothing that hides a
   title, and the page below is still where the reader left it. */
.shell-nav {
    display: block;
    box-sizing: border-box;
    background: var(--surface);
    color: var(--ink);
}

/* Heads the rail's column. Drawn at one width only: on a phone the bar above is the heading. */
.nav-cap {
    display: none;
}

/* 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.

   A COLUMN, so a long caption wraps rather than widening the menu. `Calendar — Klinik Kidz Dental
   Sehati` was the case that decided this, and its wording was SAAS-164's to shorten.

   THE SECOND SELECTOR IS THE ADMINISTRATIVE GROUP'S SUMMARY (SAAS-369) — one rule, so a group's
   name is the same target, at the same height, with the same padding as an entry that goes
   somewhere. A summary is a box before any sheet touches it, so this is what makes it the same box
   rather than what makes it a box at all. */
.nav-area,
.nav-group > summary {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: var(--s1) var(--s3);
    border-radius: var(--r-sm);
    color: inherit;
    text-decoration: none;
    white-space: normal;
}

.nav-area:hover,
.nav-group > summary:hover {
    background: var(--row-hover);
}

/* Where you are: teal on the page's own surface. The weight change carries the state for anyone who
   cannot see the tint at all. */
.nav-area[aria-current="page"] {
    background: var(--teal-wash);
    color: var(--nav-active-ink);
    font-weight: 650;
}

/* =======================================================================
   THE ADMINISTRATIVE GROUP (SAAS-369) — the one disclosure in this shell.

   ⚑ IT NARROWS ADR 0027 POINT 2, which ruled "nothing hidden, nothing to open, no <details> drawer
   at all" for the narrow shape. Darius asked for this one himself — "put people, closing days, and
   record access as a submenu for a new collapsible menu called 'Administrative'" — and what the
   ruling protected survives: the six daily surfaces are never hidden at either width, and the three
   that collapse are administration. ADR 0027's objection was a control that hid THE MENU and
   re-closed on every navigation; this one holds three entries and is open on every page inside it.

   NO SCRIPT. A <details> opens on click and on Enter, is in the tab order and announces its own
   expanded state — see ShellNav for why nav.js was not spent on it. The account menu one band up is
   the same element for the same reason.

   ⚑ SINCE SAAS-463 IT SITS INSIDE THE PHONE'S MENU, which is the one disclosure above it: the group
   is a block in the column, at both widths, so it takes the column's width without being told to
   and the chevron sits at the far end of its row, where a disclosure's marker belongs.
   ======================================================================= */
.nav-group {
    display: block;
    box-sizing: border-box;
    min-width: 0;
}

.nav-group > summary {
    /* The marker goes in both engines' spellings. Without this the browser draws its own triangle
       beside the chevron we authored — the account menu's own note. */
    list-style: none;
    cursor: pointer;
    justify-content: space-between;
    gap: var(--s2);
}

.nav-group > summary::-webkit-details-marker {
    display: none;
}

/* The chevron turns rather than being swapped for a second glyph: one path, and the open state is
   readable while the transition runs. */
.nav-group > summary .glyph {
    flex: 0 0 auto;
    transition: transform 160ms ease;
}

.nav-group[open] > summary .glyph {
    transform: rotate(180deg);
}

/* ⚠ WHERE YOU ARE, WHEN THE GROUP IS SHUT. A collapsed group that did not say it holds the current
   page would answer "which section am I in" only to a reader who opens it, which is the one thing
   a disclosure must not cost. Three carriers, because ADR 0020 never lets colour be the only thing
   saying a state: the wash and the weight the current entry already uses, and — in the markup
   rather than here — a visually-hidden clause rendered only when it is true.

   Deliberately NOT `aria-current`. That attribute names the current ITEM in a set, and the item is
   the link inside; a summary wearing it as well would put two current things in one navigation. */
.nav-group > summary.holds-current {
    background: var(--teal-wash);
    color: var(--nav-active-ink);
    font-weight: 650;
}

/* The entries inside, stacked and nested under the name they belong to, with the same rule down the
   left the views get — the two levels of nesting in this column are the same shape, so a reader
   learns one. Stretched rather than shrink-to-fit: in a column the entries are rows, and a row a
   reader can only hit across the width of its own caption is a worse target than the 44px floor
   already bought. */
.nav-group-links {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--s1);
    margin: var(--s1) 0 var(--s2) var(--s4);
    padding: 0 0 0 var(--s3);
    border-left: 1px solid var(--border-strong);
}

/* THE SECOND LEVEL, nested under its area at both widths, and the rule down the left is what says
   so — indent alone would read as a gap. Since SAAS-463 an open phone menu draws it exactly as the
   rail does: the views are inside the menu, under the area they belong to, and are not a band of
   their own beneath it (that band, with the area's name over it, was direction B's). */
.nav-second {
    display: block;
    box-sizing: border-box;
    margin: var(--s1) 0 var(--s2) var(--s4);
    padding: 0 0 0 var(--s3);
    border-left: 1px solid var(--border-strong);
}

/* AN AREA WITH NO SECOND LEVEL DRAWS NO BLOCK FOR ONE: the outlet is always rendered (its place in
   the markup must not move), and the shell cannot know whether a page filled it — a section is
   content, not a signal — so the emptiness itself is the test. */
.nav-second:not(:has(.surface-views)) {
    display: none;
}

/* 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.

   A column of rows at both widths, 44px each like the areas above them. The tab idiom's underline
   has no edge to sit on in a column, so the current view is marked by the wash the pager and the
   table rows already use for "this one". */
.surface-views {
    min-width: 0;
}

.surface-views ul {
    display: block;
    margin: 0;
    padding: 0;
    list-style: none;
}

.surface-views a.view {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 0 var(--s3);
    border-radius: var(--r-sm);
    color: var(--muted);
    text-decoration: none;
    font-size: var(--t-small);
    white-space: normal;
}

.surface-views a.view:hover {
    color: var(--ink);
    background: var(--row-hover);
}

.surface-views a.view.current {
    background: color-mix(in srgb, var(--ink) 9%, transparent);
    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 {
        flex: 0 0 232px;
        width: 232px;
        padding: var(--s4) var(--s3);
        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);
    }

    /* THE PHONE'S DISCLOSURE, UNDONE (SAAS-463). A closed <details> hides its contents in its own
       shadow tree, so at this width the bar is not drawn, the disclosure takes no box of its own,
       and the content slot is made visible and box-less — whether or not anything opened it. The
       rail is then a direct item of the row, exactly as SAAS-162 drew it.

       Behind a feature query, and the reason is the failure mode: a browser that does not know
       `::details-content` would drop these rules but keep a closed <details>, and a rail nobody
       can open is worse than a Menu button beside one. Without the query's support nothing here
       applies, so the bar stays and opens the rail. */
    @supports selector(::details-content) {
        .shell-menu {
            display: contents;
        }

        .shell-menu > summary {
            display: none;
        }

        .shell-menu::details-content {
            content-visibility: visible;
            display: contents;
        }
    }
}

/* --- 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*.

       ⚑ SAAS-463 (2026-10-02) CUT THE CHROME THIS ARGUES ABOUT from 273–329px to 94px at 375px —
       the 50px masthead and the 44px closed Menu bar — and left this rule as it was: whether a
       94px band should stick is a separate ruling, and nobody has made it.
    */
    /* ⚑ SAAS-468 (2026-10-03) REVERSED THE RULE THAT STOOD HERE. This block said `.site-header {
       position: static }` and the argument above it still reads true of the band it argued about: a
       sticky band that wraps to three rows is a fifth of a phone. What Darius chose instead is a band
       that is ONE ROW — a 56px bar holding the Menu, the page's title and the account — and a
       sticky 56px is worth having where a sticky 94px was not: it is what keeps the title, the sync
       mark and the account in reach while a calendar scrolls. So the masthead is sticky again below
       768px, the bar's own rules are in the SAAS-468 section at the foot of this sheet, and
       a sign-in page, which has no menu and no bar, keeps the static band it always had. */
    .site-header:not(.has-menu) {
        position: static;
    }

    /* AND SO A LANDED ROW OWES THE BAR'S HEIGHT (SAAS-170, SAAS-468). The offset buys clearance from
       a sticky band, and below 768px the band is the 56px bar — so a row a crumb lands on is not
       scrolled under it. The rule that spends the offset is written once and reads this token, so
       the phone case is a value rather than a second rule. */
    :root {
        --landing: var(--bar);
    }

    /* 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)));
    }

    /* THE OPEN MENU'S OWN FRAME (SAAS-463): the rail's column framed as a panel — gutters matching
       the page's, and a rule under it so the page below reads as a separate thing rather than as
       more menu. */
    .shell-nav {
        padding: var(--s2) var(--s4) var(--s3);
        border-bottom: 1px solid var(--border);
    }

    /* THE INFORMATION MARK'S PANEL, ON A PHONE (SAAS-462). The mark can sit anywhere along a line, so
       a panel hung off its left edge would run past the viewport: it stops hanging off the mark and
       takes its horizontal extent from the page's gutters instead, while its vertical place stays
       directly under the mark. */
    .info-mark {
        position: static;
    }

    .info-mark-panel {
        left: var(--s4);
        right: var(--s4);
        top: auto;
        width: auto;
        max-width: 40ch;
    }
}

/* --- 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);
}

/* =======================================================================
   A NAME THAT OPENS A THING (SAAS-363), DECLARED ONCE.

   Darius read the live portal and reported it: "Receipt no. in billings are clickable to open the
   detail of the billing, but it doesnt indicate that it is clickable, same as patient name in
   patients, make it so it does look clickable like the brance name in brances menu, with underline
   and color".

   ⚠ THE PRODUCT ALREADY HAD THE RIGHT ANSWER AND IT WAS NOT A CLASS — IT WAS AN ABSENCE. Eleven
   cells across four tables carry `a.row-link`: a Branch name into its own page, a patient's name
   into their record, a receipt number into the receipt. The Branch name looked right because
   NOTHING styled it — it fell through to `a` above and the browser's own underline. The three that
   looked wrong were the three somebody had written a rule for, each saying `color: inherit;
   text-decoration: none` and moving the underline to :hover, each with the same argument attached:
   "fifty underlined names down a column is a page of rules." The argument is real and it lost:
   fifty underlined names are a page of links, and a name that opens a record and does not say so
   is a control the reader never finds. So the treatment is one rule, and the Branch name joins it
   rather than keeping its lucky exemption.

   THE THICKENING IS THE HOVER, NOT THE UNDERLINE. The underline is already there at rest, so
   hover has to say something else; a hue change would have to come off --row-link's headroom and
   is the one thing this palette cannot afford here (see --row-link for the five measurements).

   `text-underline-offset` is the register's own figure, kept: its hover rule used 2px and this is
   that treatment made permanent rather than a new one invented beside it.
   ======================================================================= */
a.row-link {
    color: var(--row-link);
    text-decoration: underline;
    text-underline-offset: 2px;
}

a.row-link:hover,
a.row-link:focus-visible {
    text-decoration-thickness: 2px;
}

/* ----------------------------------------------------------------------
   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);
}

/* A STATED CONDITION ON THIS TABLE WEARS NO COLOUR (SAAS-236), and it is `tr.sync-unknown`'s
   argument two rules down applied to the Status column: a Branch onboarded reports-only holds no
   runtime account by design, so "reports-only — no runtime account" is how it was set up, not a
   clinic in trouble. This cell read `Access denied for user '_no_such_runtime'` in `.notice` amber
   beside three green sync columns until then — an outage notice about a Branch serving every page
   asked of it. Amber is kept for the things an Operator can act on, which on this row is a Report
   Credential that is missing or refused. */
td .stated {
    color: var(--muted);
}

tr.unreached td {
    background: color-mix(in srgb, var(--warn) 6%, transparent);
}

/* --- THE FLEET'S SYNC COLUMNS (SAAS-213, ADR 0031 ruling 3) --------------

   Three columns saying how old the copy is, beside the three saying whether the Branch answers.
   The two are independent — the ADR's finding is that a Branch can answer in 4 ms while serving a
   copy that stopped arriving six hours ago — so they are coloured independently and a row can
   legitimately wear both washes at once.

   THE WASH IS ON THE ROW, not on the cell, and that is the whole reason the sort exists: an
   Operator scans forty rows for colour before reading a single word, and a tinted cell in column
   six is not something a scan finds. Red and amber are `--bad` and `--warn`, the status ladder's
   own two, at the same 6% `tr.unreached` already uses — enough to find, not enough to make the
   text underneath fail its contrast. */

tr.sync-attention td {
    background: color-mix(in srgb, var(--warn) 6%, transparent);
}

tr.sync-incident td {
    background: color-mix(in srgb, var(--bad) 7%, transparent);
}

/* AND THE OUTCOME CELL CARRIES THE INK. A 6% wash is what `tr.unreached` has always used and it
   is right for a row tint — but a wash alone was measured on the SAAS-213 capture and is close to
   invisible in the dark theme, and "failing branches read red" is the ruling. So the one cell that
   says what the Agent's last run did takes the status ladder's own ink, which is `td .ok` and
   `td .notice`'s existing habit on this same table rather than a new idea. */
tr.sync-attention td.sync-outcome {
    color: var(--warn);
    font-weight: 600;
}

tr.sync-incident td.sync-outcome {
    color: var(--bad);
    font-weight: 600;
}

/* "We could not ask" is ACHROMATIC, deliberately, and it is the same argument `p.stale-notice
   .unmeasured` makes one screen away: a Server with no Sync Reader recorded is Daclaria's own
   missing step, not a clinic in trouble, and painting it amber would make forty un-onboarded
   Branches look like forty failures during the very rollout that creates them. */
tr.sync-unknown td.sync-cell {
    color: var(--muted);
}

/* The two narrow ones. A wrapped timestamp costs a row its scan line, and these two are what the
   scan is for. */
td.sync-cell {
    white-space: nowrap;
}

/* The Agent's own words. Truncated in the markup to a lead (SyncMessageText) with the whole of it
   in the cell's `title`, so this only has to stop the lead from setting the table's width. */
td.sync-said {
    white-space: normal;
    max-width: 32ch;
}

/* --- ONE BRANCH'S RUN HISTORY (SAAS-213) ---------------------------------

   The Agent's own log on a screen. It reuses `table.branches` wholesale — same row height, same
   rule, same heading — because it IS the same kind of table and a second set of measurements is
   how two tables in one realm come to disagree about a row.

   The failed row is washed for the fleet page's reason and at the fleet page's strength. An open
   row is not: a run in flight is the normal state for one row on every visit, and a wash that fires
   on the ordinary case is a wash a reader stops seeing. */
table.sync-runs tr.run-failed td {
    background: color-mix(in srgb, var(--bad) 7%, transparent);
}

td .bad {
    color: var(--bad);
    font-weight: 600;
}

/* The Agent's own name for what went wrong — `verification-failed`, `sync-failed`. A label rather
   than prose, because it is the word an Operator searches the Agent's README for, and monospace
   because it is an identifier rather than a sentence. */
.run-signal {
    display: inline-block;
    margin-right: var(--s2);
    padding: 0 var(--s2);
    border: 1px solid color-mix(in srgb, var(--bad) 40%, transparent);
    border-radius: var(--r-sm);
    font-family: var(--mono);
    font-size: var(--t-small);
    color: var(--bad);
    white-space: nowrap;
}

/* THE SCHEMA-CHANGE MARKER (SAAS-253). A chip beside the Branch's name on the fleet row and on
   its own page, in --warn rather than --bad: nothing has broken. The clinic's structure moved and
   the Agent rebuilt the copy inside the hour; what is owed is a look, not a fix.

   It is deliberately NOT a row wash. The row's washes are freshness states, and putting a schema
   change in that channel would file it as a sync problem and set it beside the incident somebody
   has to act on tonight. */
.schema-changed {
    display: inline-block;
    margin-left: var(--s2);
    padding: 0 var(--s2);
    border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
    border-radius: var(--r-sm);
    background: color-mix(in srgb, var(--warn) 8%, transparent);
    font-size: var(--t-small);
    color: var(--warn);
    white-space: nowrap;
}

/* One probed reading's verdict. `reads` is achromatic on purpose — a probe over a healthy clinic
   is fifteen rows of it, and fifteen green ticks is a wall a reader stops seeing, which is exactly
   how the one red line in the middle gets missed. */
.probe-reads {
    color: var(--muted);
}

.probe-fails {
    color: var(--bad);
    font-weight: 600;
}

/* MySQL's own words, kept as an identifier rather than set as prose: it names a column. */
td.probe-said {
    font-family: var(--mono);
    font-size: var(--t-small);
}

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 SYNC'S WASH, ON THIS TABLE TOO (SAAS-390). Written out here rather than left to the
   generic `tr.sync-attention td` rules the Operator fleet table uses, because `table.freshness
   tr.unreached td` above is a more specific selector and would otherwise beat them on a row that
   is both unmeasured and behind. Same two colours at the same two strengths, so the Operator's
   fleet page and an Owner's branch list cannot disagree about one Branch.

   These rules follow `tr.unreached` on purpose: equal specificity, so source order decides, and a
   Branch whose sync is genuinely behind reads as behind rather than as merely unmeasured. */
table.freshness tr.sync-attention td {
    background: color-mix(in srgb, var(--warn) 6%, transparent);
}

table.freshness tr.sync-incident td {
    background: color-mix(in srgb, var(--bad) 7%, transparent);
}

/* AND THE SYNC CELL CARRIES THE INK — `tr.sync-attention td.sync-outcome`'s habit on the fleet
   table, for its measured reason: a 6% wash alone is close to invisible in the dark theme, and
   "the sync has not succeeded" is the one thing this row now raises its voice about. */
table.freshness tr.sync-attention td.copy-synced-cell {
    color: var(--warn);
    font-weight: 600;
}

table.freshness tr.sync-incident td.copy-synced-cell {
    color: var(--bad);
    font-weight: 600;
}

/* THE CONTENT PILL, AND IT IS ACHROMATIC IN EVERY STATE (SAAS-390).

   It used to come in three weights — green, amber, red — off `FreshnessWeights`, which mapped the
   five-rung ladder onto "keeping up / drifting / broken". That mapping is retired with its C# half,
   and the reason is Darius's ruling of 2026-09-24: what the ladder measures is the CLINIC'S OWN
   ACTIVITY, and painting a clinic that has been quiet in the colour of a broken sync is the portal
   crying wolf. A JNJ Batam row read red "more than 24 hours" ninety minutes after a successful sync,
   because one of its five sources had had no new row for four days.

   So the pill states the rung in the page's own ink and wears the shape alone. The colour that
   readers scan for now lives one column to the left, where the Agent's own word is. */
.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;
}

/* 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 stalest probed table — the one the "Oldest source" cell names in words (renamed from "Last
   received" by SAAS-390, because stalest is what it holds). This is the same fact where the sources
   are listed. Emphasis, never colour: it is the clinic's own activity, not a fault of ours. */
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);
}

/* The Agent's one word per source — "quiet" or "not through the last sync" (SAAS-410). Drawn
   exactly as the age beside it: the same separator, the same weight, the same muted ink, and NO
   COLOUR OF ITS OWN. Colour on this page is the Sync Concern's alone (SAAS-390), and a stopped
   table is a fact to read, not an alarm to paint. */
ul.sources .outcome::before {
    content: "·";
    margin-inline-end: var(--s2);
    color: var(--border-strong);
}

ul.sources .outcome {
    font-weight: 400;
    color: var(--muted);
}

p.stated {
    margin: 0 0 var(--s1);
    max-width: 44ch;
    color: var(--muted);
    font-size: var(--t-body);
}

/* The payer comparison's limitation (SAAS-392) is a paragraph of prose rather than a condition of
   a few words, so it is given a reading measure instead of the stated sentence's 44ch. It stays a
   `.stated` in every other respect — muted, achromatic, no pill — because it is a caveat about a
   figure and not a fault. */
p.basis-caveat {
    max-width: 68ch;
    margin: var(--s2) 0 0;
}

/* 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'S OWN RULES LEFT WITH IT (SAAS-462). `.legend` drew the "How to read this" section under the
   Branches table — a bordered block with a small-caps heading and a two-column grid. The key now sits
   behind the information mark beside the page title, inside `.info-mark-panel`, which carries the
   spacing and the term weight a definition list there needs (SAAS-390's rule that a key's terms read
   heavier than their definitions is kept: `.info-mark-panel dt`). Nothing else drew a `.legend`. */

.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 .field > .field-head > 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:not(.info-mark-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:not(.info-mark-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:not(.info-mark-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;
}

/* ⚑ A SCREEN RULE (SAAS-467). Unqualified, this block also matched a printed A4 or Letter page (718
   to 739px wide, both under 900) and stacked the list above the form on paper, where the two columns
   fit: the page's own sentence "Choose a report on the left." then pointed at a list printed ABOVE
   it. Printed at A4 portrait as it was and with the block under `screen` (the SAAS-458 harness), the
   stacked sheet is the phone's arrangement and the scoped one the desktop's, whole and unclipped.
   Paper is not a phone. */
@media screen and (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;
}

/* "Separate each branch" (SAAS-391), under the branch list whose reading it changes.
   No sizing and no clipping of any kind: this sits inside the branch selection, which SAAS-380
   took the pane away from, and anything that scrolled here would clip the freshness tooltip
   again. */
.separate-branches {
    margin: var(--s2) 0 0;
    font-size: var(--t-body);
}

.separate-branches label {
    display: flex;
    align-items: center;
    gap: var(--s2);
    text-transform: none;
    letter-spacing: normal;
    font-size: inherit;
    color: inherit;
    margin: 0;
}

.separate-branches .small {
    display: block;
    margin-top: var(--s1);
}

/* ⚠ THE BRANCH SELECTION IS NOT A PANE, AND THAT IS SAAS-380's WHOLE FIX.
   =========================================================================
   It used to cap itself at `max-height: 17rem` with `overflow-y: auto`. Darius, 2026-09-19, having
   watched the freshness sentence open inside it: "the mouse over should expand outside of the
   pane, now its buggy because its inside a pane, or just put the whole branch selection outside."

   HE OFFERED TWO REMEDIES AND THEY TURNED OUT TO BE ONE. The first — make the hover escape the
   pane — is not buildable while the pane exists: a box cannot escape an ancestor that scrolls,
   because `overflow` clips every descendant whose containing block lies inside it, so no amount of
   `position: absolute` gets the sentence out. Escaping a live scroll container needs
   `position: fixed` with anchor positioning, or the top layer through `popover` — which opens on a
   CLICK, not the hover he asked for.

   SO THE SECOND REMEDY IS WHAT DELIVERS THE FIRST. Take the pane away and an ordinary absolutely
   positioned tooltip simply works, with no second layout and nothing to maintain. He asked for
   exactly that one message later — "put this inside a mouse over" — and it was already available,
   because of this. See `.stale-marker-tip`.

   WHAT THE CAP WAS WORTH, COUNTED RATHER THAN ARGUED. 17rem is 272px and a row is about 29px, so
   it bit at ten clinics. The largest Tenant in the fleet holds ELEVEN. The cap therefore bought
   about 60px of page height, on one Tenant, in exchange for a scroll container wrapped around the
   one control on the page that has something to open inside it.

   AND THE SIDEWAYS CLIP WENT WITH THE CAP, WHICH IS NOT TIDYING EITHER. SAAS-375 wrote
   `overflow-x: clip` here against a horizontal scrollbar nobody had declared — it arrived because
   `overflow-y: auto` coerces a `visible` on the other axis to `auto`. With no cap there is no
   non-`visible` value left to coerce it, so the scrollbar cannot recur. And `clip` had to go
   anyway: a clip is a clipping CONTEXT, and the freshness sentence is now a layer anchored to a row
   inside this list, so `clip` would have cut it off sideways exactly as `auto` cut it off
   vertically before. An uncapped list overflows VISIBLY, which for a tooltip is the whole point and
   for a clinic name never arises, because names wrap.

   ⚠ SO THE RULE BELOW CARRIES NO SIZING AND NO OVERFLOW AT ALL, and the emptiness is the promise
   rather than an omission. Write a height cap or an overflow of any kind on either axis and this
   becomes a scroll container again, the tooltip is clipped, and SAAS-375 and SAAS-380 both come
   back. BranchPickerScrollTests asserts the absence directly, so it fails rather than the page. */
ul.branches-select {
    list-style: none;
    margin: 0;
    padding: 0;
}

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);
}

/* THE MARK BESIDE THE BUTTON (SAAS-478, fresh-eyes review 2026-10-08). The download's information
   mark is the paragraph's next sibling, so the shared `p:has(+ .info-mark)` rule should put the two on
   one line — but `p.download` above has the same specificity and comes later, so the paragraph stayed
   a flex BLOCK and the mark fell to the line under the button, on Reports and on Day End, after every
   run. Inline here, with the higher specificity, and the flex row inside it is kept. */
p.download:has(+ .info-mark) {
    display: inline-flex;
    vertical-align: middle;
}

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:not(.subtotal-row):hover td {
    background: var(--row-hover);
}

/* --- Subtotal by (SAAS-395) ------------------------------------------------ */

/* A subtotal row closes one value's lines inside a Branch block. It takes the band's own idiom at a
   smaller scale — weight and the same 3% wash of ink, achromatic for the reason at the top of this
   section — so it reads as a sum and never as one more line. No heavier rule above it: a doctor
   list of fourteen would draw fourteen, and the band's full-strength rule is kept for the one row
   that is the fleet's. Not lifted on hover: like the fleet row, it is not a line to compare against
   its neighbours. */
.report-results tr.subtotal-row td {
    font-weight: 650;
    color: var(--ink-strong);
    background: color-mix(in srgb, var(--ink) 3%, transparent);
}

/* The label sits in the subtotal column itself — "drani Total (54 lines)" — and is not allowed to
   wrap: a label broken over two lines doubles the row's height at every value. */
.report-results tr.subtotal-row td.subtotal-label {
    white-space: nowrap;
}

/* The band's second table, "All branches by Issued by": a heading one step below the band's own, so
   it reads as part of the band rather than as a block of its own. */
.report-results .branch-block.summary h4.by-value {
    font-size: var(--t-lead);
    font-weight: 650;
    color: var(--ink-strong);
    margin: var(--s5) 0 var(--s2);
}

/* --- 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,
p.subtotal-stale {
    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);
}

/* A Subtotal by the run did not return (SAAS-395): one of the boxed statements above, since it is
   the same kind of sentence — something the page will not do, said in words — and it stands above
   the band, where the subtotals would have begun. */
p.subtotal-stale {
    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 branch list's sync column. It used to be told not to out-shout the pill beside it; since
   SAAS-390 it is the column that shouts, and the pill does not shout at all. Quiet by default all
   the same — most rows are healthy — and the two rules that raise its voice are up beside the
   freshness table's own washes, keyed on the row's SyncConcern rather than on a boolean this cell
   used to carry. */
td.copy-synced-cell {
    color: var(--muted);
    white-space: nowrap;
}

/* --- 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);
}

/* THE BRANCH COLUMN OF A REGISTER READ FOR SEVERAL CLINICS (SAAS-512). Darius, 2026-10-09: "many clinics at once, but add
   the branch name to the table if it is more than one branch". It leads the table, like the Clinic column of the Billing
   and Appointments lists, and a clinic's name is one unbroken line: it is the half of a row's identity that makes the
   patient number and the name readable at all (two clinics both hold a patient 10041). Ordinary ink and weight -- the
   patient's name is the handle and stays the heaviest thing on the row. */
table.register th.clinic,
table.register td.clinic {
    white-space: nowrap;
}

/* 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), and it is drawn by `a.row-link`
   alone since SAAS-363 — the rule that used to sit here said `color: inherit; text-decoration:
   none` and waited for a hover, which is exactly the defect Darius reported. See A NAME THAT OPENS
   A THING for why the argument this cell once made — "the cell is already the heaviest ink in the
   row, so it reads as the thing to click without being told" — did not survive being read. */

/* --- 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;
}

/* THE NOTE THE DESK LEFT ON THE BILL keeps the breaks it was typed with (SAAS-470, found by the
   fresh-eyes review of 2026-10-08): about 20,550 live Puri Indah bills carry a break inside their
   Remark, and the generic `dd` collapsed every one to a space. pre-line, not pre, for the patient
   notes' reason below: the stored breaks are kept and a long line still wraps to the column. The
   markup draws the value alone on one line, so the template's own whitespace adds no blank lines. */
.detail-group dd.remark {
    white-space: pre-line;
    overflow-wrap: anywhere;
}

/* 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;
}

/* A VISIT'S CLINICAL NOTES, ON ONE RECORD'S PAGE AND ON THE RECORDS LIST (SAAS-470) — for the same
   reason as the standing notes above. They had no rule at all, so every break a clinician typed
   collapsed to a space and a note written as a list read as one paragraph; Darius, 2026-10-03:
   "medical record texts also lose its line break". Only the breaks: the dl's own spacing is the
   detail-group's, and TypedTextKeepsItsBreaksTests holds every block of typed text to this. */
.record-notes .note {
    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;
    /* SAAS-474: three buttons do not fit a 375 px dialog, and without wrapping the first one
       (Cancel) was pushed off the left edge. They wrap, still right-aligned. */
    flex-wrap: wrap;
    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);
}

/* THE SIXTH CHIP, AND IT IS NOT A SURFACE (SAAS-251, ADR 0017 addendum 2026-09-06). Drawn only
   when the Group holds the grant, so it needs no `off` state — and set apart from the six beside it
   by a rule rather than by a second hue, because the brand's one colour is already spent on the
   chips' granted state and on the action. It is the same pill; what says "different kind of thing"
   is the border, which is the quietest signal that survives the dark theme unchanged. */
.portal-people .grant-chips li.people-chip {
    border-color: var(--border-strong);
    background: transparent;
    color: var(--ink);
}

/* THE EIGHTH AND NINTH CHIPS, AND THEY MEAN THE OPPOSITE OF THE SEVEN BESIDE THEM (SAAS-359,
   ADR 0025 addendum of 2026-09-18).

   ⚠ WHY THEY ARE NOT `.on`. Every chip above says "this group opens X". These two say "this group is
   kept from X", and a reader scanning the row as a list of capabilities would read "Own patients
   only" as something extra rather than something withheld. On a clinical axis that inversion is the
   one this card cannot afford, so the two are a different KIND of chip and look like one.

   SOLID INK ON A WASH OF THE PAGE, with no teal anywhere near them. Teal is the brand's one hue and
   ADR 0020 spends it on the primary action and on status; a grant wears it here and a limit must not,
   or the difference between "opens" and "is kept from" would be carried by a word alone. The wash is
   --page, which in both themes sits a step BACK from the card the chip is on — a recessed pill
   rather than a lit one — and the border is --border-strong so it still reads as a deliberate state
   and not as the dashed `.off`, which means "not given" rather than "taken away".

   NO `off` STATE, the People and Targets chips' rule: both ticks are off on every Group in the fleet
   the day this ships, so a chip that was off almost everywhere would be noise on every card. */
.portal-people .grant-chips li.limit {
    border-color: var(--border-strong);
    background: var(--page);
    color: var(--ink);
    font-weight: 600;
}

/* WHY A ROW HAS NO ACTIONS (SAAS-251 ruling 1). A person on branches this reader does not manage
   stays on the screen and says so — hidden, the reader cannot tell "not here" from "not mine". It
   sits in the actions cell, in the actions' own line height, so the table's rhythm does not break
   where the buttons stop. Quiet: it is a boundary working correctly, not a warning. */
.portal-people .locked-row,
.portal-people .locked-note {
    display: block;
    max-width: 34ch;
    font-size: var(--t-small);
    color: var(--muted);
    font-style: italic;
}

.portal-people .locked-note {
    margin: 0;
}

.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,
.portal-people .field > .field-head > label,
.form-dialog .field > label,
.form-dialog .field > .field-head > 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;
}

/* WHO PUT THIS PERSON HERE, when it was not the Owner (SAAS-233, ADR 0017 addendum 2026-09-04).
   Beneath the name rather than beside it, so a row's first line stays the person and the second is
   provenance — and quiet, because the fact is informative rather than something to act on. The
   Owner keeps every power over such a row; the sentence is not a warning. */
.vendor-invited {
    display: block;
    margin-top: 2px;
    font-size: var(--t-small);
    color: var(--muted);
    font-style: italic;
}

/* 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;
}

/* A CHOICE THIS EDITOR MAY NOT MAKE IS DRAWN, NOT HIDDEN (SAAS-474, Darius's ruling of 2026-10-08):
   the doctor's account, for a person linked at a clinic the editor does not manage. Muted, with a
   not-allowed cursor, and the sentence that says why sits under it, in line with the radio's own
   words. Muted ink on the surface is the portal's quiet pairing and is not a status colour. */
.form-dialog .doctor-pick label.shape:has(input:disabled) {
    color: var(--muted);
    cursor: not-allowed;
}

.form-dialog .doctor-pick p.doctor-kind-refused {
    margin: 0 0 var(--s2) var(--s5);
}

/* THE KIND OF ACCOUNT, FIRST (SAAS-474, Darius's ruling in comment 11343). Three cards in a row on
   a wide screen, stacked on a phone. Each is a label around its radio, so the whole card is the
   target. The checked card takes the action colour's edge and wash — teal is chrome and action
   (ADR 0020) — so the reader sees which kind the questions below belong to. */
.form-dialog .kind-pick {
    margin: 0 0 var(--s4);
    padding: 0;
    border: 0;
    min-width: 0;
}

.form-dialog .kind-pick legend {
    padding: 0 0 var(--s2);
    font-size: var(--t-lead);
    font-weight: 600;
    color: var(--ink);
}

.form-dialog .kind-pick .kinds {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--s3);
}

.form-dialog .kind-pick label.kind {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    column-gap: var(--s2);
    row-gap: var(--s1);
    padding: var(--s3);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-md);
    background: var(--surface);
    cursor: pointer;
}

.form-dialog .kind-pick label.kind input {
    margin: 0;
    width: auto;
    accent-color: var(--action);
}

.form-dialog .kind-pick .kind-title {
    font-size: var(--t-body);
    font-weight: 600;
    color: var(--ink);
}

.form-dialog .kind-pick .kind-says {
    grid-column: 2;
    font-size: var(--t-small);
    line-height: 1.45;
    color: var(--muted);
}

.form-dialog .kind-pick label.kind:has(input:checked) {
    border-color: var(--action);
    box-shadow: inset 0 0 0 1px var(--action);
    background: var(--teal-wash);
}

.form-dialog .kind-pick label.kind:has(input:focus-visible) {
    outline: 2px solid var(--action);
    outline-offset: 2px;
}

/* Name, Email, Password and Group, two to a row on a wide screen — the drawing's grid. */
.form-dialog .person-fields {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--s4);
}

/* The phone breakpoint, written in range syntax: the calendar's tests find "the" `(max-width:
   767px)` block by its first appearance, and this one is earlier in the file.

   ⚑ A SCREEN RULE (SAAS-467). Written in range syntax, so a search for `max-width` does not find it.
   Unqualified, it also matched a printed A4 or Letter page, and a dialog printed while open came out as
   one stacked column, taller than it needs to be, where two columns fit. */
@media screen and (width <= 767px) {
    .form-dialog .kind-pick .kinds,
    .form-dialog .person-fields,
    /* ...and a clinic's name above its doctor picker, so the picker can show a whole name. One
       class more specific than the two-column rule further down, which it has to beat. */
    .form-dialog .doctor-pick ul.doctor-select li {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* ONLY THE SECTIONS THE CHECKED KIND NEEDS (SAAS-474). Each section names the kinds it is asked of
   in `data-asked-of`; the form's checked kind radio hides every section that does not name it. No
   script: a reader without one still gets a working form, and a browser without :has() shows
   every section — the form as it was. The handler is the truth whatever is shown. */
.form-dialog form:has([data-doctor-shape="ordinary"]:checked) [data-asked-of]:not([data-asked-of~="ordinary"]),
.form-dialog form:has([data-doctor-shape="doctor"]:checked) [data-asked-of]:not([data-asked-of~="doctor"]),
.form-dialog form:has([data-doctor-shape="practising"]:checked) [data-asked-of]:not([data-asked-of~="practising"]) {
    display: none;
}

/* ...and the doctor question in the words of the kind that asks it: one legend at a time. */
.form-dialog form:has([data-doctor-shape="doctor"]:checked) [data-legend-of]:not([data-legend-of="doctor"]),
.form-dialog form:has([data-doctor-shape="practising"]:checked) [data-legend-of]:not([data-legend-of="practising"]),
.form-dialog form:has([data-doctor-shape="ordinary"]:checked) [data-legend-of] {
    display: none;
}

/* The question a doctor's account is asked when it stops being one (SAAS-474 ruling 5): the
   Managed Branches frame, with its explanation under the legend. */
.form-dialog .leaving-doctor .hint {
    margin: 0 0 var(--s2);
}

/* 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.

   ⚠ ONE ENTRY IS NAMED BY ITS CLASS RATHER THAN BY A CONTAINER, and it is the only one:
   `.action.danger`. See THE DESTRUCTIVE TREATMENT FOLLOWS THE CLASS below for why that one is the
   exception. It sits in THIS list so a destructive control's geometry is declared once, here,
   rather than spelled a second time down there — two copies of a control's padding is how a
   modifier drifts from the thing it modifies.

   ⚠ AND THE LAST TWO ENTRIES ARE THE PRIMARY MODIFIER ON THE TWO PORTAL PAGES THE CONTAINER LIST
   DID NOT REACH (SAAS-370) — they are here for `.action.danger`'s reason and nothing else: the
   modifier has to bring its geometry, or it lands as a teal fill with no box, no padding and no
   44px floor. See THE PRIMARY TREATMENT IS SCOPED TO THE PORTAL'S PAGES below for why those two
   are named rather than the modifier unscoped. */
.portal-people .action,
.form-dialog .action,
.portal-account .action,
.portal-closing-days .action.primary,
.portal-branch .action.primary,
.action.danger {
    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);
}

/* =======================================================================
   THE PRIMARY TREATMENT IS SCOPED TO THE PORTAL'S PAGES, BY NAME (SAAS-370)

   One filled button per dialog, and one on the page: the thing this screen is for.

   ⚠ WHY IT IS NOT UNSCOPED, WHICH IS THE WHOLE OF THIS TICKET. `.action.primary` was scoped
   exactly the way `.action.danger` was before SAAS-355 — `.portal-people` and `.form-dialog`, the
   same container list that was never a decision — so `Add a closing day…` on Closing days carries
   `class="action primary"` in the markup and drew as a PLAIN LINK. It is visible in SAAS-355's own
   captures.

   Forty-four controls in the two realms wear `action primary`. Forty of them sit inside a
   `FormDialog`, so `.form-dialog` already reaches them; one more is the New group trigger inside
   `.portal-people`. UNSCOPING WOULD THEREFORE CHANGE EXACTLY THREE MORE, and one of the three is
   in the VENDOR realm: `Add a closing day…` on the Operator's own Targets screen, which is the
   same control on the vendor's copy of the same page. A vendor-realm control changing appearance
   is a decision for that realm and this ticket does not carry it, so the two portal pages are
   named here instead.

   ⚑ WHAT THE EVIDENCE SAYS ABOUT THAT, WRITTEN DOWN BECAUSE IT POINTS THE OTHER WAY. SAAS-229's
   ruling — plain row links, separated by `.action-sep` rather than boxed — is about ANCHORS wearing
   bare `.action` in the Operator LIBRARY tables, and it is the argument that keeps the treatment
   above container-scoped. It says nothing about a page-head trigger marked `primary`, and
   `operator.css` already specifies exactly that role for that realm: `.operator-actions
   .button.primary`, "the one filled control on a list screen: the trigger that opens its entry
   dialog". So the vendor's `Add a closing day…` is mis-classed markup rather than a deliberately
   plain control, and unscoping would hand it the treatment its own realm already asks for. That is
   a one-line change and it is the Operator realm's to make.

   THE TWO PORTAL PAGES, AND WHY THEIR PLAIN `.action` SIBLINGS ARE LEFT ALONE. `.portal-branch`'s
   and `.portal-closing-days`'s bare `.action` controls are NOT in the treatment rule above, and
   that is this ticket's boundary rather than an oversight — but the two cases differ and the
   difference is worth knowing before somebody "finishes the job":

     · Closing days must NOT have it. `Rename…` is a bare `.action` in the same cell as the
       destructive `Delete…`, and SAAS-355's whole argument is that the BOX is the shape difference
       a reader who sees no red still reads. Giving `Rename…` the same box would leave hue as the
       only carrier and put that page back where it began.
     · The Branch page could. Its three controls — `Show`, `Save targets`, `Save closed days` — are
       `<button class="action">` and `<button class="action primary">`, and a `<button>` that no
       rule reaches draws in the BROWSER'S own chrome rather than as anything plain. The two
       `primary` ones are fixed here; `Show` is still a native button, which is a defect of its own
       and not this modifier's.
   ======================================================================= */
.portal-people .action.primary,
.form-dialog .action.primary,
.portal-closing-days .action.primary,
.portal-branch .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,
.portal-closing-days .action.primary:hover,
.portal-branch .action.primary:hover {
    border-color: transparent;
    background: var(--action-hover);
}

/* =======================================================================
   THE DESTRUCTIVE TREATMENT FOLLOWS THE CLASS, NOT THE CONTAINER (SAAS-355)

   Outlined rather than filled: a filled red on every panel would be the loudest thing on a screen
   whose subject is people. --bad measures 6.57:1 on --surface and 6.19:1 on --page in light, 7.74:1
   and 8.41:1 in dark — both backdrops, because this rule is no longer only drawn on a panel.

   ⚠ WHY THIS ONE IS UNSCOPED WHILE THE TREATMENT ABOVE IS NOT. This rule read
   `.portal-people .action.danger, .form-dialog .action.danger` for a year, and that list was never
   a decision: it began inside the People screen and the second entry is a RENAME's leftovers
   (`.portal-dialog` → `.form-dialog`, e1e5d5e). Closing days is neither container, so the page
   that most needed a destructive affordance was the page the style could not reach — `Rename…` and
   `Remove…` drew in the same hue, the same weight and within half a pixel of the same width, and
   the person who specified the product read his own page and counted two Remove links (SAAS-355).
   A Closure moves every Branch's Target to date (ADR 0033), so that is money on a mis-click.

   A semantic style that must be opted into per page is a style that will be missed. So this one
   follows the class: a destructive action is destructive wherever it is drawn, and a page cannot
   forget to be dangerous.

   ⚠ AND WHY THE TREATMENT ABOVE STAYS CONTAINER-SCOPED, which is the other half of the ruling and
   the half that is easy to get wrong. Unscoping bare `.action` would restyle the whole vendor realm:
   the Operator library's row actions wear `class="action"` and have always drawn as PLAIN LINKS,
   deliberately — SAAS-229 met the very same two-links-read-as-one defect there and ruled it with a
   separator (`.action-sep`, operator.css) rather than with boxes, and that realm has its own control
   treatment in `.operator-actions .button`. Turning several dozen vendor-side links into outlined
   buttons is a design decision for the vendor realm, not a side effect of a portal defect fix.

   THE BORDER IS NOT DECORATION, it is the second carrier. ADR 0020 puts colour on state and never
   makes colour the only thing saying it. Red against this page's teal is the hue difference; the
   box is the SHAPE difference, and it is the one a reader who sees no red still reads. That is why
   the modifier brings the geometry with it (it is the fourth entry in the base rule above) rather
   than landing as bare red text wherever the container list does not reach.
   ======================================================================= */
.action.danger {
    color: var(--bad);
    border-color: color-mix(in srgb, var(--bad) 45%, transparent);
}

.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:not(:disabled) {
    background: var(--row-hover);
}

/* INERT WHILE NOTHING IS TICKED (SAAS-367). The report form's own weight for the same state,
   because a control that cannot be pressed has to look like one — a live-looking button that
   does nothing is worse than no button. Only dialogs.js ever sets this: the server draws the
   button live whatever the selection, since a disabled control with no script to re-enable it
   is a form that cannot be filled in. */
.branch-form button:disabled {
    opacity: .5;
    cursor: default;
}

/* CLEAR AND THE COUNT, ABOVE THE LIST (SAAS-367) — the row Reports and Day End already use,
   which carries buttons there and a link here. The link gets the button's own no-wrap so
   "Clear" and "0 of 3 selected" cannot break across two lines at 375px. */
.select-all a {
    white-space: nowrap;
    flex: 0 0 auto;
}

/* WHY THE BUTTON WILL NOT HELP. Louder than `p.stated`'s muted grey, because it is the one
   sentence standing between a reader and a button that has stopped responding — and it is
   drawn by the server whenever the server already knows, so it is reachable with no script.

   ⚠ WRITTEN `p.stated.branch-none` AND NOT `.branch-none`, WHICH IS A FIX (found while measuring
   SAAS-373). `p.stated` sets `color: var(--muted)` and scores (0,1,1); a bare `.branch-none` scores
   (0,1,0) and loses to it however far down the sheet it sits, so this rule has been shipping its
   margin and NOT its colour since SAAS-367. The comment above said "louder than p.stated's muted
   grey" and the rendering was that grey exactly. One class more, and it says what it says. */
p.stated.branch-none {
    margin: var(--s2) 0 0;
    color: var(--bad);
}

/* WHY THE SUBMIT DID NOTHING (SAAS-373) — "Choose at least one date — a billing range cannot be
   empty." The Branch picker's own refusal above, same weight and same reason: it is the sentence
   standing between a reader and a click that will not answer, and the server draws it whenever the
   server already knows, so it is reachable with no script.

   TWO SELECTORS, ONE SENTENCE. `.range-empty` is the copy beside the boxes, inside the form;
   `.range-refused` is the copy where the rows would have been. They are the same words in the same
   ink because they are the same refusal, said once beside the control and once over the gap.

   The first sits under the hint rather than replacing it, because the hint is about the range that
   WILL be read and this is about the one that will not. */
p.stated.range-empty,
p.stated.range-refused {
    margin: var(--s2) 0 0;
    color: var(--bad);
}

/* 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 .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);
}

/* A RECEIPT NUMBER'S MATCHES, HEADED FOR WHAT THEY ARE (SAAS-472). One heading over the list — the
   number as typed, "every date", and the count beside it — and one control that clears the number
   and nothing else. Layout is Darius's drawing of 2026-10-03: the heading on the left, the control at
   the right edge of the same row, wrapping under it on a phone. */
.portal-billing .number-found {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s2) var(--s4);
    margin: 0 0 var(--s3);
}

.portal-billing .number-found-heading {
    margin: 0;
    font-size: var(--t-body);
    font-weight: 600;
    line-height: 1.4;
    color: var(--ink);
}

.portal-billing .number-found-count {
    font-weight: 400;
    color: var(--muted);
}

/* A BOX, so the coarse-pointer floor on `a` reaches it — it is the whole of its own control, a
   target rather than a word in a sentence (TapTargetFloorTests). */
.portal-billing a.clear-number {
    display: inline-flex;
    align-items: center;
    font-size: var(--t-body);
    white-space: nowrap;
}

/* The sentence the sort control gives way to while a number fixes the order — the control's own
   line, in the narrowed line's own muted weight. */
.portal-billing p.sort-fixed {
    margin: 0 0 var(--s3);
    font-size: var(--t-body);
    line-height: 1.5;
    color: var(--muted);
}

/* THE DATES STAY ON SCREEN AND SAY THEY ARE NOT APPLIED (SAAS-472). The boxes and the quick ranges are
   dimmed, not removed: they hold what was sent and are what the list returns to the moment the number
   is cleared. The sentence is state, kept visible — amber on the EDGE and ink on the words, for the
   sync mark's reason above: --warn measures 4.32:1 as text against the page, and the words must read. */
.ledger-filters .field-range.dates-idle .range,
.ledger-filters .field-range.dates-idle .quick-ranges {
    opacity: 0.55;
}

.ledger-filters p.stated.dates-not-applied {
    margin: var(--s2) 0 0;
    padding-left: var(--s2);
    border-left: 3px solid var(--warn);
    color: var(--ink);
    font-weight: 500;
}

/* 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. */

/* IT SCROLLS RATHER THAN SQUEEZES, and the pair of widths is what decides which.

   `width: 100%` on its own forces every column inside the container, so a table with one column too
   many pays for it in ROW HEIGHT: the short cells wrap and the whole ledger grows. Measured when the
   remark column made the receipts view eleven columns wide — at 1344px the rows went from 42px to
   63px, half again as tall, on the table an Owner scans most.

   `max-content` sizes the table to what its cells actually hold, and `.ledger-scroll` above is
   already an `overflow: auto` box, so the overflow becomes a horizontal scroll instead of a wrap.
   That box exists for exactly this and its max-height is what makes the sticky headings work.
   `min-width: 100%` keeps the other half of the old behaviour: a table with room to spare still
   fills the width rather than huddling at the left.

   THIS IS ONLY BOUNDED BECAUSE EVERY FREE-TEXT CELL IS CLAMPED. `max-content` on a column of
   unclamped prose would be a table thousands of pixels wide. The one such column on any of these
   tables is `td.remark`, whose span stops at 26rem — so the widest a ledger can become is the sum of
   its bounded columns. A future column of loose text must clamp itself the same way or this breaks
   quietly, in the one direction nobody looks: rightwards, off the screen. */
table.ledger {
    width: max-content;
    min-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 DESK'S OWN NOTE, ON ALL THREE MONEY VIEWS. Muted, and it has to recede across a very wide
   spread of how full it is: 8-18% of Indo Dental Center Pluit's receipts a year since 2019, 15% at
   Evit HangLekir, and 100% at Pluit before 2019, where the desktop composed a treatment line into
   the field itself. So the same column is four em dashes in five on a recent range and a wall of
   machine-written text on an old one. Muted, it reads as context for a figure in both places, which
   is what it is. It is never the figure.

   CLAMPED TO ONE LINE, WITH THE WHOLE NOTE ON THE CELL'S TITLE. Free text has no width — 28
   characters on average across Pluit's post-2019 remarks, 25 at HangLekir, 251 at the longest
   measured — and one such note allowed to wrap would set the height of a row whose other nine cells
   are one line. 26rem carries better than four fifths of them whole and ellipsises the tail of the
   rest. The clamp is on the inner span rather than on the cell, because `max-width` on a `td` is a
   suggestion an auto-layout table may ignore and a block span is not.

   IT IS THE LAST COLUMN ON EVERY VIEW, so the horizontal scroller cuts it first: it is the one
   column a reader can lose the tail of and still have read the row. */
table.ledger td.remark {
    color: var(--muted);
}

table.ledger td.remark > span {
    display: block;
    max-width: 26rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* 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), and it is drawn by
   `a.row-link` alone since SAAS-363. The rule that used to sit here was a copy of the register's —
   which is how the defect spread to six tables from one, and why the treatment is now declared
   once. See A NAME THAT OPENS A THING. */

/* --- What was done at one visit, with no price (SAAS-365) -----------------

   The FIRST rules this Surface's record page has ever carried. Everything else on it — the crumb,
   the cards, the clinical notes, the stated sentences — is `.crumb`, `.detail-group` and `p.stated`
   doing their work, which is why there was nothing here before and why there is very little now.

   ⚠ MEASURED, AND THE MEASUREMENT DOES NOT LET THIS OFF. The 2026-08-11 ruling refused PRICED items
   inline at 2.8× taller on the worst record. Taking the money off does NOT rescue that, because the
   height is the LINE COUNT and not the price column: on the fleet's worst record — Puri Indah's
   21-line bill — this block takes the receipts card from 102px to 593px at 1440px and from 144px to
   635px at 375px, which is the whole record page 2.08× taller at 1440px against the rejection's 2.8×.
   Same order of magnitude, not a tenth of it.

   So this slice reverses a ruling on that ruling's own grounds, and it does so because Darius asked
   for it on 2026-09-18, which is later. What he is buying is in the slice's report with these
   figures beside the rejection's, and the remedies if the height is the problem are named there
   rather than chosen here.

   `list-style: none` and no indent of its own, because the list is ALREADY inside a bulleted
   `li.receipt`: left to the browser it drew hollow circles 80px in from the card, which reads as an
   outline rather than as what was done. The receipt it belongs to is what indents it. */
.visit-items {
    margin: var(--s1) 0 0;
    padding: 0;
    list-style: none;
}

/* WHICH KIND THE LINES BELOW ARE (SAAS-365 amendment). Darius ruled "Both, separated" on
   2026-09-19, so each receipt now draws up to two labelled groups instead of one flat list.

   ⚠ IT IS A `p` AND NOT AN `h5`, AND THAT IS THE HEADING OUTLINE RATHER THAN A PREFERENCE. This
   block sits inside `li.receipt`, inside a list, inside the receipts `<section>` which already
   carries the page's `h2`. A real heading here would put "Services" in the document outline
   between a patient's visit and their clinical notes, so a screen reader's heading list would fill
   with the word "Services" once per bill in the record. The lines it labels are an unordered list
   with its own boundary; what this needs to do is NAME them, not rank them.

   A STEP DOWN AND MUTED, in the label register this Surface uses for every field name — so the eye
   still runs down what was done and the kind is there for the reader who wants it. Uppercase with
   letter-spacing would be the filter labels' register, which belongs to controls rather than to
   clinical content.

   The top margin is bigger than the bottom on purpose: the label belongs to the list under it, so
   it must sit nearer that list than to the group above. Only the FIRST group's label sits under the
   receipt line, and `:first-of-type` pulls that one tight because the receipt line above is already
   spaced. */
.visit-items-kind {
    margin: var(--s2) 0 0;
    color: var(--muted);
    font-size: var(--t-small);
    font-weight: 600;
}

/* ⚠ SCOPED TO `li.receipt`, AND THAT IS SAAS-383 RATHER THAN TIDYING. `:first-of-type` matches the
   first `p` among its siblings, and the chronology draws these groups inside `li.record`, which
   ALREADY holds one or two `p`s of its own — `p.record-by` for the clinician, `p.stated` for a visit
   with no notes. So on a chronology entry the rule fired only on the entries that happen to carry
   neither, which is a gap above "Services" that changed depending on whether the clinic recorded
   who wrote the note. Unscoped it was a rule about the markup's shape; scoped it is a rule about the
   receipt line it was written for. On the chronology every group takes the `--s2` gap, which also
   separates the billing from the notes above it. */
.receipt .visit-items-kind:first-of-type {
    margin-top: var(--s1);
}

/* A labelled group's list sits against its own label rather than carrying the gap twice. */
.visit-items-kind + .visit-items {
    margin-top: 0;
}

/* One item, one line. `--ink` and not `--muted`: this is clinical content and the reader came for
   it — the muted ink belongs to the labels and the stated sentences, not to what was done. */
.visit-item {
    color: var(--ink);
    overflow-wrap: anywhere;
}

/* HOW MANY, WHERE THERE IS MORE THAN ONE. A step down and muted, so the eye runs down the names and
   the count is there for the reader who needs it — and so that nothing on this block can be mistaken
   for the money that is deliberately absent from it. Tabular figures for the same reason every other
   number on this Surface has them.

   ⚠ NO `margin-left`, DELIBERATELY. The gap is a non-breaking space inside the span, put there
   because a margin is not a character: without it the line's TEXT read "zzSikat Gigi Gum Summit× 6"
   for a screen reader, a copy-paste and the test, while looking correct on screen. One mechanism
   makes the space, so there is no second one to keep in step. `white-space: nowrap` then keeps the
   count with the name it counts rather than letting a bare "× 6" wrap onto its own line. */
.visit-item .item-count {
    color: var(--muted);
    font-size: var(--t-small);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* A BILL WITH NO LIVE LINE SAYS SO, and it says it where the list would have been. `p.stated` is
   already `--muted`; this only stops it inheriting the receipt line's own spacing and sitting hard
   against the number above it. 445 of Puri Indah's 95,813 live receipts are in this state — every
   voided one among them — so it is an ordinary sentence and not a failure. */
p.stated.visit-nothing-itemised {
    margin: var(--s1) 0 0;
}

/* WHAT EVERY VISIT WAS BILLED FOR COULD NOT BE READ, SAID ONCE ABOVE THE CHRONOLOGY (SAAS-383).
   `p.stated` is already `--muted` and carries the sentence's register; this only keeps it off the
   sort-choice line above and the first visit below, so it reads as a condition of the page rather
   than as part of the first entry. It is drawn once per page and never per entry — see the markup,
   where that is the ruling rather than the styling. */
p.stated.visit-items-unreadable {
    margin: var(--s2) 0 var(--s1);
}

/* --- 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,
.receipt-payments .ledger-scroll {
    margin-top: var(--s1);
}

/* THE PAYMENTS TABLE (SAAS-459) is the items table's twin and shares its rules below rather than
   restating them: it sits one section lower on the same page, in the same box, read the same way —
   a short table of money against a document. It is a table of its own and not a variant of
   `table.items` so that a selector aimed at one cannot count the other's rows. */
table.items,
table.payments {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--t-body);
    font-variant-numeric: tabular-nums;
}

table.items th,
table.items td,
table.payments th,
table.payments td {
    padding: var(--row-y) var(--s3);
    border-bottom: 1px solid var(--line);
    text-align: left;
    vertical-align: baseline;
}

table.items thead th,
table.payments 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,
table.payments td.num,
table.payments th.num {
    text-align: right;
    padding-inline-start: var(--s5);
}

table.items tbody tr:last-child td,
table.payments tbody tr:last-child td {
    border-bottom: 0;
}

table.items td.item-name {
    color: var(--ink-strong);
    min-width: 14rem;
}

table.items td.amount,
table.payments 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,
table.payments td.payment-staff .staff-login {
    display: block;
    margin-top: var(--s1);
    font-size: var(--t-small);
    color: var(--muted);
    font-variant-numeric: normal;
}

/* THE PAYMENTS TABLE'S OWN COLUMNS (SAAS-459). "Taken by" gets the staff column's floor, for its
   reason: a name runs to 41 characters across the fleet and is allowed to wrap rather than push the
   amount off a phone.

   THE REMARK is the desk's own note on one payment, free text of no fixed width — 251 characters at
   the longest the Payments tab measured against a 25-character average — so it wraps here and is
   not clamped to a line the way the tab's is: this is a document and not a ledger of fifty rows,
   and a note cut off with an ellipsis on the page somebody opens to answer a patient's question
   would be the wrong way round. It keeps the line breaks somebody typed (`pre-line`), as a line's
   note on the items does, and it recedes into muted for the reason the tab's does: on most rows it
   is a dash.

   NOT MEASURED. It reuses tokens already on this page and adds no colour, which is an argument
   rather than a contrast sweep or a 375px scrollWidth check. */
table.payments td.payment-staff,
table.payments th.payment-staff {
    min-width: 9rem;
}

table.payments td.remark {
    min-width: 10rem;
    color: var(--muted);
    white-space: pre-line;
    overflow-wrap: anywhere;
}

/* 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.

   A SCREEN RULE (SAAS-458). Unqualified, this matched a portrait A4 page (about 719px) as well as a
   phone, and a printed receipt came off with its money stacked and left-aligned under the labels
   instead of in a column of figures against the right edge: Subtotal, Total, Collected, Balance and
   State each over their amount, a line taller apiece. Paper has the room the grid wants, and a
   receipt's figures belong in a column. Printed with this block under `screen`, the amounts align
   right as they do on a desktop. */
@media screen and (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;
}

/* The sentence and its mark as one block (SAAS-478): the block carries the sentence's gap above, so
   the sentence inside it carries none, and the More sheet's column holds the pair as one child. */
.cal-one-clinic-line {
    margin: var(--s1) 0 0;
}

.cal-one-clinic-line > .cal-one-clinic {
    margin: 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;
}

/* The same words, for paper's title line (SAAS-513): the date line above sits among the controls, which do not
   print. Not drawn on a screen, where that line says it. */
.cal-title-date {
    display: none;
}

/* The merged Narrowed sentence's words for paper (SAAS-507): "in the key below", where a screen says "ticked below".
   Not drawn on a screen; the calendar's print block draws it and hides the screen's. */
.narrowed .on-paper {
    display: none;
}

/* The two boxes a printed day's grid sits in (SAAS-513) draw nothing on a screen: the grid is where it was. */
.cal-room,
.cal-fit {
    display: contents;
}

.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 one-clinic calendar has the axis pair after the mode pair (SAAS-521), where the merged calendar has no axis
   pair at all: each pair is set apart from the one before it by the same rule. */
.cal-views + .cal-axes {
    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;
}

/* A DAY IS NEVER NARROWER THAN ITS LANES, AND A DAY WITH MORE CLINICIANS TAKES MORE OF THE WEEK (SAAS-514).

   THE FAULT. The rule above gives every day an equal share (`flex: 1 1 0`) and lets it shrink to nothing
   (`min-width: 0`), while each lane heading and each lane holds `--cal-lane-min`. The frame asks for the
   SUM of every day's lanes and hands it out in equal shares, so the shortfall falls on the one day that
   has more of them: at about 1010px a Wednesday with three clinicians was given the 167px a Monday with
   one gets and needed 350px, and at 1920px it was given 267px. Its lanes overflowed it (the overflow is visible), so the headings and the
   absolutely placed blocks were drawn over Thursday. It dates from SAAS-78 and was measured in a browser
   at 1010, 1280, 1440, 1920 and 375px.

   THE FIX HAS TWO HALVES, BOTH KEYED TO HOW MANY LANES THE DAY DRAWS (`--cal-lanes`, written on the panel by
   AppointmentCalendarGrid; a day nothing is booked on draws none and says one, because "Nothing booked" is
   a sentence that has to fit). The grow factor is the lane count, so free space is shared by lane and a
   three-clinician day is not squeezed to a one-clinician day's width. The minimum is what the lanes need,
   so when the week does not fit, `.cal-frame`'s `min-content` is real and the card scrolls sideways
   (SAAS-458) instead of drawing one day over the next.

   WHAT A LANE NEEDS is not `--cal-lane-min` alone. The heading is a content box with `--s1` of padding a
   side and a rule before every heading but the first, so a lane asks for 108 + 8 = 116px and each lane
   after the first one pixel more; for n lanes that is n x (108 + 8 + 1) - 1. The minimum is read from
   `--cal-lane-min`, never restated, so the phone's 84px (SAAS-468) is what it was.

   WHY THE MINIMUM IS THERE AT ALL, MEASURED. Sharing out by lane count is most of the cure, because the
   frame's `min-content` already sums what every lane truly needs. But the share lands within a pixel of
   the need, not on it: with this minimum at 0 (or counting only the 108), a three-clinician Wednesday at
   1010px and at 375px was 1.09px short, its last heading ending at 634.09px against a day edge of 633px.
   The minimum is what closes that last pixel, and it is the part that survives a week that does not fit.

   A SCREEN RULE, AND PAPER NEVER SEES IT. A printed week sets `--cal-lane-min` to 0 and lets a day take the
   width its lanes ask for (`flex: 1 1 auto`, SAAS-458, in the print block far below); the declarations
   here sit in `@media screen` so that block is not countermanded, shadowed or re-measured by this one.
   The merged week needs none of this: it draws one lane a day and its `.mw-day` already carries
   `min-width: var(--mw-lane-min)` and no `min-width: 0` (measured at 1010, 1440 and 1920px, SAAS-514). */
@media screen {
    .cal-day {
        --cal-lane-need: calc(var(--cal-lane-min) + var(--s1) * 2 + 1px);
        flex: var(--cal-lanes, 1) 1 0;
        min-width: calc(var(--cal-lanes, 1) * var(--cal-lane-need) - 1px);
    }
}

.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);
}

/* THE HOUR RULES ARE FOR PAPER (SAAS-525). Each hour of a day panel has an element with a border, drawn ahead of the lanes by
   both grids, and a screen does not draw them: its hour lines are the gradients above, which cost nothing in the DOM. Paper draws
   these and not the gradient, in the print block far below, because a gradient is a background. */
.cal-rules,
.mw-rules {
    display: none;
}

/* THE FIRST LANE IS THE ONE AFTER THE HOUR RULES (SAAS-525): both grids draw the rules as the body's first child, so the first lane is no
   longer `:first-child`, and a rule that only said so drew a second line beside the gutter. */
.cal-lane:first-child,
.cal-rules + .cal-lane {
    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;
}

/* LINE ONE OF A BLOCK: WHEN, AND WHO IS SEEING THE PATIENT (SAAS-394). The two share one line box,
   which is the whole reason the clinician is drawn on every block whatever its duration — it costs no
   height. It is a flex row rather than two inline spans so that the NAME is the thing that gives way
   when the lane is narrow: only a flex item can be told to shrink while its neighbour does not.

   ⚑ A HEIGHT GATE WAS TRIED FIRST AND THE MEASUREMENT CONDEMNED IT. The clinician was a fourth LINE,
   drawn only where the block had the room. The threshold was measured honestly in a browser against
   this sheet — a --t-micro line box is 12.65px, a block's chrome is 4px, four lines need 54.6px, and
   at 54px the browser clips the fourth by 0.563px — and then the durations: of 106,009 live bookings
   at Puri Indah 77.6% are under 45 minutes and 19.0% are 45 to 60, so at 0.9px a minute a 60-minute
   block is 54.0px and missed by 0.6px. "Where it has room" meant rarely. Darius withdrew the
   condition on 2026-09-24. Left written here because the next element somebody wants to gate on this
   grid's height will look like a good idea for the same reasons this one did.

   THE GAP IS THE SCALE'S SMALLEST STEP AND THAT IS MEASURED TOO. A 108px lane leaves a block 96px of
   content and the time takes 31.5, so the gap is the whole difference between the longest thing line
   one ever carries fitting and not: "no clinician" wants 58.59px, which one step of the scale leaves
   room for (60.5) and the next step up does not (58.5, an ellipsis 0.09px from the end of a word). */
.cal-block .cal-topline {
    display: flex;
    gap: var(--s1);
    align-items: baseline;
}

/* The time never gives way. It is what a reader scans a column for, and it is four glyphs wide
   whatever the booking. */
.cal-block .cal-when {
    flex: 0 0 auto;
    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;
}

/* WHO IS SEEING THE PATIENT (SAAS-394). The lane heading names the clinician on the clinician axis,
   but time runs down and a week is seven panels, so an afternoon block is a long way from its own
   heading — and on the room axis the heading is a room, and the clinician was nowhere on the page.
   The name is set apart from the patient's: the patient is the block's subject and keeps the weight,
   and a second name in the same weight would compete with it.

   IT TAKES WHAT LINE ONE HAS LEFT AFTER THE TIME, and never more, so it cannot push the block a line
   taller than the height the layout engine drew it at — the line under it would be clipped instead.

   ⚠ min-width: 0 IS THE LOAD-BEARING DECLARATION IN THIS RULE, and it is measured. A flex item's
   floor is its own content width until it is told otherwise, and this row has no overflow of its own
   to zero that automatically. Take it away and the name refuses to shrink: at a 108px lane a
   39-character name stays 229.48px wide and pushes the count 182.25px PAST the block's edge, where
   the block's own overflow: hidden erases it — a booking shared between two clinicians reading as
   naming its only one, which is the exact failure the ruling forbids. Even a ten-character name puts
   the count 10.3px outside. */
.cal-block .cal-with {
    display: flex;
    flex: 0 1 auto;
    gap: 3px;
    align-items: baseline;
    min-width: 0;
    color: var(--muted);
}

/* A booking the desk recorded no clinician against SAYS SO, in the no-length marker's own manner —
   italic, because it is a fact about what was recorded rather than about the appointment.

   IT IS ITS OWN ELEMENT RATHER THAN A MODIFIER ON THE ROW ABOVE, and that is measurement rather than
   taste. "no clinician" is 58.9 px at this size and line one leaves 58.5 px of a 108 px lane once the
   time has taken its 31.5 — and a phone's lane is 84 px, where it leaves 34.5. A flex ROW cannot
   ellipsise its own text, so the words would have been clipped mid-glyph with nothing to say they
   had been. This shortens with an ellipsis, like every other name on the block. */
.cal-block .cal-without {
    flex: 0 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--muted);
    font-style: italic;
}

/* The name shrinks and ellipsises... and it needs no min-width of its own, unlike the row above:
   overflow: hidden already zeroes a flex item's automatic minimum size, which was checked by taking
   this rule's min-width: 0 away and re-measuring — the layout did not move by a pixel. */
.cal-block .cal-with-name {
    flex: 0 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ...and the count of the clinicians NOT named never does. Line one is narrower than the whole block
   — a lane is 108 px and the time takes about 35 — so a long name is going to be shortened; if the
   count went with it the block would read as naming the only clinician on a booking that has two. So
   the name gives way and the count stays. Measured at the narrower line one: with a name 40
   characters long the count still draws its full 14.27 px, inside the block. */
.cal-block .cal-with-more {
    flex: 0 0 auto;
    font-variant-numeric: tabular-nums;
}

/* 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;
}

/* ⚑ THE PER-CLINIC BLOCK OPENS THE PATIENT TOO, SO IT IS THE SAME ANCHOR AS THE MERGED GRID'S
   (SAAS-449) — SAAS-378's ruling, carried to the sibling rendering that was left an `<article>`:
   "instead of the name, make the block into a link instead". Its reasons are `a.mw-block`'s, above,
   and are not repeated here: with the whole block inside an anchor the browser underlines every
   line of it, and a decoration set on an ancestor cannot be removed by the spans under it. This
   block's own spans already set their own colours (`.cal-when` and `.cal-what` are --muted,
   `.cal-who` is --ink-strong) and the block sets --ink, which beats `a`'s colour on specificity, so
   only the decoration moves. One declaration, as there.

   ⚠ NOTHING HERE MAY TOUCH THE BOX, and the floor's `min-height` is taken back off this class too
   inside the `pointer: coarse` block at the foot of the sheet — see `a.cal-block` there, dated, with
   the reason and the measurement. */
a.cal-block {
    text-decoration: none;
}

/* 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 USED TO SAY SO HERE, AND NO LONGER DOES — Darius on
   2026-09-25 (SAAS-421), shown the block rendered: "i dont like the 'no length' text, just dont put
   anything if there is no duration". SAAS-161's rule is overruled rather than forgotten, and its
   reason is kept because it has not gone away: a block's height IS its duration, and a booking with
   none is floored like every other block, so unmarked it reads as a SHORT appointment rather than an
   unknown one. Before the floor it was drawn one row tall, which stated the fact in a way nobody
   could read. The fact now lives in the block's title, in full. This grid puts no duration on the
   block at all — its height is the whole statement — so nothing replaced the rule that was here. */

/* 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;
}

/* THE CLINIC PICKER (SAAS-509; ONE COMPONENT FOR EVERY PAGE BY SAAS-510): a line that says how many clinics are on screen,
   and opens onto a checklist of them. The merged calendar's clinic key is the first of them. It sits above both of that
   page's renderings, because a reader who switches modes should not have to relearn the key. ONE LINE SINCE SAAS-496
   (Darius, 2026-10-08: "30 branch shows at the same time is stupid, do a summary"): how many clinics and bookings and a
   clinic that could not be read, on a line that opens onto the key, a list sorted by name.

   THE LINE IS A DROPDOWN, AND THE DROPDOWN IS THE CLINIC PICKER (SAAS-509; Darius, 2026-10-09: "just make the new
   control into a checklist that the user can tick", ruled "Click to open, Show inside"; then "change the branch filter to
   be like this everywhere else, its better than what we have now"). A <details>: it opens on a click or a tap and a hover
   opens nothing, so there is no `:hover` or `:focus-within` rule on the panel anywhere in this block
   (MergedClinicKeyStylesheetTests reads the sheet for one).

   WHAT IS THE CALENDAR'S OWN is named `mw-` (the swatch, the code, the count, the hue of a row, the key's print) and the
   calendar's key wears `mw-legend` on the picker's root; what every page shares is `clinic-picker`. */
.clinic-picker {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s1) var(--s3);
    margin: 0 0 var(--s3);
    font-size: var(--t-small);
}

/* The form around the dropdown draws nothing: it is the page's GET form, and its box is the dropdown's. */
.clinic-picker-form {
    min-width: 0;
    margin: 0;
}

.clinic-picker-menu {
    position: relative;
    min-width: 0;
}

/* The line is the summary: text, not the round ⓘ — a button with a border, the sync mark's pattern (SAAS-469). */
.clinic-picker-menu > summary {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    box-sizing: border-box;
    min-height: 32px;
    padding: 0 var(--s2);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink-strong);
    font-weight: 600;
    white-space: nowrap;
    list-style: none;
    cursor: pointer;
}

.clinic-picker-menu > summary::-webkit-details-marker {
    display: none;
}

.clinic-picker-menu > summary:hover,
.clinic-picker-menu[open] > summary {
    background: var(--row-hover);
}

.clinic-picker-glyph {
    flex: 0 0 auto;
    color: var(--muted);
    transition: transform .15s ease;
}

.clinic-picker-menu[open] .clinic-picker-glyph {
    transform: rotate(180deg);
}

/* THE LINE HAS TWO SIZES (SAAS-522; Darius, 2026-10-09, "Shorter on phones"). The summary holds the whole sentence ("29 of 30
   clinics selected · 208 bookings") and, after it, the short one ("29/30 clinics"); a wide screen and paper draw the first, and
   below 768 px the phone block draws the second and clips the first, which a screen reader still reads. The short one is
   `aria-hidden`, so the sentence is heard once. */
.clinic-picker-short {
    display: none;
}

/* The panel hangs off the line, over the page: the tick list, and Show under it. Wider than an explanation's 40ch because a
   clinic's full name, its code and its count share a row. It opens when the details does and for no other reason.

   ⚠ IT HAS NO HEIGHT CAP AND NO OVERFLOW, and the emptiness is the promise (SAAS-375, SAAS-380, and BranchPickerScrollTests
   for the list it replaced). A row may carry a freshness marker, whose sentence is a layer anchored to the row; any box
   around the rows that scrolls or clips cuts the sentence off. A long list makes the page longer, and Show is held at the
   foot of the screen (below). A list with no marker may say so, and scroll inside the panel: the merged calendar's key
   (`.clinic-picker-scrolls`). */
.clinic-picker-panel {
    position: absolute;
    z-index: 30;
    top: calc(100% + var(--s1));
    left: 0;
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    width: max-content;
    min-width: min(18rem, calc(100vw - var(--s6)));
    max-width: min(36rem, calc(100vw - var(--s6)));
    margin: 0;
    padding: var(--s3);
    background: var(--surface);
    color: var(--ink);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    box-shadow: var(--e2);
    font-size: var(--t-small);
    font-weight: 400;
    line-height: 1.5;
    letter-spacing: 0;
    text-transform: none;
    text-align: start;
}

/* The ticks (the fieldset dialogs.js works on) take what the panel has, and where the key scrolls it is the list inside them
   that does. */
.clinic-picker-group {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
    margin: 0;
    padding: 0;
    border: 0;
}

.clinic-picker-group .select-all {
    flex: 0 0 auto;
    margin: 0 0 var(--s2);
}

.clinic-picker-group .select-all a {
    color: var(--action);
}

/* THE LIVE PICKER'S SELECT ALL AND CLEAR (SAAS-524, Reports and Day End) ARE BUTTONS THAT LOOK LIKE THE LINKS ABOVE: there is no
   address for a link to follow on a page whose ticks are circuit events, and a link with no destination is a button wearing a
   disguise. So they are `<button class="clear">`, drawn as the static pages draw theirs: the action colour, underlined, no box.
   Three classes, because `.report-form button:not(.info-mark-button)` (the page's outlined buttons, 0,2,1) is what they stand
   inside and would otherwise be drawn by; its disabled rule (half opacity) is the one they keep. */
.clinic-picker-group .select-all button.clear {
    padding: 0;
    border: 0;
    background: none;
    color: var(--action);
    font: inherit;
    text-decoration: underline;
    white-space: nowrap;
    cursor: pointer;
}

.clinic-picker-group .select-all button.clear:disabled {
    cursor: default;
}

.clinic-picker-group .branch-none {
    flex: 0 0 auto;
    margin: var(--s2) 0 0;
}

.clinic-picker-list {
    flex: 1 1 auto;
    min-height: 0;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* A page's own list class may draw a grid (`.branch-form .branches-select` is four columns of a wide form): in the panel it
   is one column of rows. Later in the sheet than that rule, so it wins on equal weight. */
.clinic-picker-panel .clinic-picker-list {
    display: block;
}

/* THE KEY'S LIST SCROLLS INSIDE THE PANEL, so Show stays in view on a phone and the panel never runs under the bottom bar.

   ⚠ THE BOUND IS A CHAIN OF GRID TRACKS, AND A FLEX CHAIN WAS NOT ONE (SAAS-522, from fresh-eyes review 7 at `9bca2030`). The
   panel was a flex column with a `max-height`, the fieldset a flex item with `min-height: 0`, the list a flex item of the
   fieldset with `min-height: 0` and `overflow-y: auto`; every link that should let the list shrink was in place and the list
   did not. Measured at 375 px with 29 of 30 clinics ticked: panel 131..579 (its cap), fieldset 144..512 (it shrank), list
   196..1516 (it did not), so 1320 px of rows in a 316 px track, `overflow-y: auto` never biting because the list was as tall
   as its content, and rows K10–K30 painting over Show and the bookings. A fieldset's anonymous content box keeps its content's
   height when a FLEX parent shrinks the fieldset. A grid row of `minmax(0, 1fr)` hands the fieldset a definite height instead,
   and the flex list inside it then shrinks to what is left, whatever else the panel carries (the warning of an open-on-load key
   with nothing ticked, a phone's 44 px links). That one rule is the load-bearing one: a grid of the fieldset's own, tried
   beside it, changed nothing. A doctor's key has no fieldset: its list is the panel's one child and takes the first row. Print
   undoes it (the block below).

   (The test that was meant to hold this passed on the broken sheet: it asked whether the PANEL's scrollHeight was larger than
   its clientHeight, which is true of any panel whose content runs out of it. ClinicPickerContainmentProbe reads the list's own
   overflow, and where the rows paint.) */
.clinic-picker-scrolls .clinic-picker-panel {
    display: grid;
    grid-template-rows: minmax(0, 1fr) auto;
    max-height: min(70vh, 32rem);
}

.clinic-picker-scrolls .clinic-picker-list {
    overflow-y: auto;
}

.mw-legend-list {
    /* How many characters the longest printed row runs (the component writes the real figure inline): paper sizes its
       cells from it. 24 is a short name and its count, for a list drawn without one. */
    --mw-key-chars: 24;
}

/* A row is a label, so a tap anywhere on it ticks the clinic. A flex box, not an inline one, so the phone's 44 px floor can
   hold (`min-height` does nothing to an inline box). The key's rows are grids of their own (`.mw-legend-row`, later). */
.clinic-picker-row {
    display: flex;
    align-items: center;
    gap: var(--s2);
    cursor: pointer;
}

/* Columns, so thirty names line up whatever their codes' lengths: [tick box,] swatch, code, name, count. */
.mw-legend-row {
    display: grid;
    grid-template-columns: 10px 3.5em minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--s2);
    padding: 2px 0;
}

/* The key's picker row has the tick box before the swatch. */
.clinic-picker-group .mw-legend-row {
    grid-template-columns: auto 10px 3.5em minmax(0, 1fr) auto;
}

.clinic-picker-check {
    margin: 0;
}

.mw-legend-row .mw-legend-count {
    text-align: end;
}

/* A clinic that is not on the week (unticked, or moved): no hue, so its swatch is an empty box and its code is ink,
   and it shows no count. Ticking it and pressing Show gives it its place among the clinics on the week. */
.mw-legend-off {
    --branch: var(--muted);
}

.mw-legend-off .mw-swatch {
    background: transparent;
    box-shadow: inset 0 0 0 1.5px var(--muted);
}

.mw-legend-moved,
.mw-legend-moved .mw-legend-name {
    color: var(--muted);
}

/* Show, under the list. Outlined, not filled — `.branch-form button`'s own rule and its reason: it is how a reader
   reaches the thing the page is for, not the page's one primary action. Inert while nothing is ticked, and only
   dialogs.js ever sets that (SAAS-367: a disabled control with no script to re-enable it is a form that cannot be
   filled in). */
.clinic-picker-foot {
    position: sticky;
    bottom: 0;
    display: flex;
    flex: 0 0 auto;
    justify-content: flex-end;
    margin-top: var(--s2);
    padding-top: var(--s2);
    border-top: 1px solid var(--border);
    background: var(--surface);
}

.clinic-picker-foot 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;
}

.clinic-picker-foot button:hover:not(:disabled) {
    background: var(--row-hover);
}

.clinic-picker-foot button:disabled {
    opacity: .5;
    cursor: default;
}

/* "N not read", inside the summary after the count, with the bullet the line uses between its parts. */
.mw-legend-unread {
    color: var(--warn);
    font-weight: 600;
}

.mw-legend-unread::before {
    content: "· ";
}

/* THE ✕ THAT SHOWS EVERY CLINIC AGAIN (SAAS-496, folding SAAS-473's phone chip into the line). A square
   target a finger can hit, the cross in the chip's muted ink. It is beside the dropdown on the line, never inside
   its summary. */
.clinic-picker-clear {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    margin-inline-start: calc(var(--s3) * -1);
    color: var(--muted);
    font-size: var(--t-body);
    line-height: 1;
    text-decoration: none;
}

.clinic-picker-clear:hover {
    color: var(--ink-strong);
}

/* ON A PHONE (SAAS-509). The line stays above the grid on the first screen. The panel opens under it and takes the
   page's gutters instead of hanging off the line's left edge, as the information mark's panel does (SAAS-462), and
   it is capped so it ends above the bottom bar even at thirty clinics. Each row is a 44 px target (SAAS-163's floor,
   which the coarse-pointer block spends on links and controls and leaves off labels). */
@media screen and (max-width: 767.98px) {
    .clinic-picker-menu {
        position: static;
    }

    .clinic-picker-panel {
        top: auto;
        left: var(--s4);
        right: var(--s4);
        width: auto;
        min-width: 0;
        max-width: none;
    }

    .clinic-picker-scrolls .clinic-picker-panel {
        max-height: min(60vh, 28rem);
    }

    .clinic-picker-row,
    .mw-legend-row {
        min-height: 44px;
        padding: 0;
    }

    .clinic-picker-group .select-all a,
    .clinic-picker-group .select-all button.clear {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
    }

    /* THE LINE IS SHORT ON A PHONE (SAAS-522): "29/30 clinics" and, beside it, "· 3 not read" where there are clinics not read.
       The whole sentence ("29 of 30 clinics selected · 208 bookings") was 80 px of two lines at 360, 375 and 414 px and pushed
       the page sideways at 320 px; this is one line with the ✕ beside it, at every phone width. It is the `.visually-hidden`
       clip recipe, not `display: none`, because it is what the summary is NAMED by. */
    .clinic-picker-short {
        display: inline;
    }

    .clinic-picker-has-short {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
    }
}

.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 PERIOD IS SIX TEN-MINUTE ROWS
   OF THE PITCH MergedCalendarLook.SlotPx DECLARES, read from `--mw-slot` which MergedWeekGrid writes
   inline (SAAS-480) — exactly as the per-Branch grid reads `--cal-slot`. It used to be the literal
   `54px`, a second copy of the nine-pixel pitch in a file no compiler reads, and at twelve pixels it
   would have drawn a line every 54px against an hour of 72. */
.mw-body {
    position: relative;
    display: flex;
    background-image: repeating-linear-gradient(
        to bottom,
        var(--border) 0,
        var(--border) 1px,
        transparent 1px,
        transparent calc(var(--mw-slot) * 6));
}

/* 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;

    /* ⚑ THE BLOCK IS A SIZE CONTAINER (SAAS-421, Darius's ruling of 2026-10-02), so a rule can be
       keyed to the BLOCK'S OWN WIDTH — see `.mw-top .mw-len` below, which is the one rule that is.
       Inline-size containment is what that needs and no more: the block's width is already written
       inline (`Placement()`), so nothing about its size can now depend on what is inside it, and its
       height, which IS the booking's duration, is not contained at all. */
    container: mw-block / inline-size;
}

/* ⚑ THE BLOCK OPENS THE PATIENT, SO THE BLOCK IS THE ANCHOR (SAAS-378) — Darius, 2026-09-18:
   "instead of the name, make the block into a link instead". Shown the four options drawn over real
   markup at a real 375px lane, he ruled the whole block rather than the name inside it.

   ⚠ THIS RULE EXISTS BECAUSE OF THE ONE COST THE DRAWING FOUND AND THE TICKET HAD NOT. With the
   whole block inside an anchor, the time, the clinic code, the length and the treatment are all
   link text: `a` gets the browser's own underline, and a decoration set on an ancestor CANNOT be
   removed by the spans under it. So a record turned into a button — five lines of underlined text
   where a reader was scanning a diary. Measured: the six interior spans' computed colours are
   already their own (`.mw-top` is --muted, `.mw-who` is --ink-strong, `.mw-what` is --muted), so the
   colour never moved and ONLY the decoration did. One declaration, therefore, rather than a copy of
   the block's palette: take the underline off and the block reads as the record it is.

   ⚠ AND NOTHING HERE MAY TOUCH THE BOX. A block's `height` IS its duration and its `top` IS its
   start time, straight out of `Placement()`. Anything that moved either would draw a 30-minute
   booking as if it ran 45 minutes, over the top of the next one — which is the option Darius killed
   on the way to this one. The floor's own `min-height` is the one thing that WOULD have moved it,
   and it is taken back off inside the `pointer: coarse` block below, dated, with the reason.

   No hover state: `:focus-visible` already rings every control in this portal and the block is now
   one, but a hover treatment for the block was not ruled and is not invented here. */
a.mw-block {
    text-decoration: none;
}

/* Time, clinic, practitioner and length on one line, IN THAT ORDER (SAAS-421, Darius on 2026-09-25:
   "put doctor name first before the duration"). The time is what a diary is scanned down, the clinic
   is what changes between the blocks, the practitioner is who the reader came for, and the length is
   last because the block's own height already states it. A booking the desk recorded no length for
   draws no fourth item at all, so line one is three things there. Which item shortens is settled by
   the flex rules below rather than by this order. */
.mw-top {
    display: flex;
    align-items: baseline;
    gap: var(--s1);
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

/* The time never gives way. It is what a diary is scanned down, and it is five glyphs whatever the
   booking. */
.mw-when {
    flex: 0 0 auto;
    font-weight: 600;
}

/* WHO IS SEEING THE PATIENT, ON THE MERGED GRID TOO (SAAS-415). SAAS-394 put the clinician on line
   one of the per-clinic grid and the merged grid was left out of that ticket's scope, so the same
   ruling applies here unchanged: every block names its clinician, at every duration, with no height
   condition — the name shares line one's box, so it costs no height at all. Measured on this sheet:
   a block with a type is 50.25px with the name and 50.25px without it, identical.

   ⚠ LINE ONE HERE CARRIES FOUR THINGS, NOT TWO, AND THAT IS THE DIFFERENCE FROM .cal-topline. The
   per-clinic grid's line one had a time and gained a name. This one already had the time and the
   clinic code, and it had a no-length marker until SAAS-421 withdrew it. Measured against this sheet
   at 11px Inter: the time is 31.42px, a four-letter code 33.98 and "30m" 23.91, the gaps are 4px
   each, and a day column at --mw-lane-min leaves a block 129px of content.

   ⚑ RE-MEASURED AFTER SAAS-421 reordered the row and took the marker off, at that 129px box:
       one clinician, 30m           name 27.69                          duration 23.91
       two clinicians, 30m          name 10.42, count 14.27 whole       duration 23.91
       one clinician, no length     name 43.50 WHOLE                    nothing drawn
       two clinicians, no length    name 38.33, count 14.27 whole       nothing drawn
   The marker cost 48.48px of the 129, so dropping it is what buys the last two rows. At 1280 and
   1440 every name but a 39-character one draws whole.

   ⚑ AND IT ENDS THE ONE CLIPPED COMBINATION SAAS-415 RECORDED. Two clinicians AND no recorded
   length, at the minimum column, used to leave the clinician 3.12px: the name went to nothing and
   6.15px of the 14.27px count was erased by the block's own overflow — a shared booking reading as
   naming its only clinician. Measured across every shape at 1920, 1440, 1280, 1100, 900, 800, 768,
   767 and 375, the count is now erased by 0.00px everywhere.

   NO ITEM SHRINKS TO MAKE ROOM FOR THE NAME, AND THE DURATION IS DRAWN WHOLE OR NOT AT ALL. The
   time is what a diary is scanned down; the clinic code is the block's identity, which the legend
   and the colour chip are keyed to; and a duration that shrank would be a WRONG NUMBER, because
   flexbox shrinks glyph by glyph. Measured: given a shrink weight, the duration lands at 8.13px at
   the minimum column and the browser paints a bare "3". So the clinician is the only flexible item
   on the row, and what runs out of room is the name — never the count beside it.

   ⚑ "WHOLE OR NOT AT ALL" IS NOW TRUE AT EVERY WIDTH, which it was not until the second ruling on
   this ticket. SAAS-421's first build left the duration `flex: 0 0 auto` and nothing to take it
   off, so a block too narrow for it let the block's own `overflow: hidden` cut it mid-glyph:
   SAAS-458's landscape PDF printed a lane-sharing 30-minute block as `10:00 WJ 3`. Darius, on
   2026-10-02, shown the three options: DROP THE DURATION OUTRIGHT, so the block reads `10:00 WJ
   drg. Sari` — not an ellipsis (a bare "3…" is the same wrong number), not the clipped glyph. The
   rule is the container query below.

   Scoped to .mw-top because .mw-code is also the legend's and the agenda's — the same few letters
   in all three places, which is what makes the key readable, and none of the other two is this row. */
.mw-top .mw-code {
    flex: 0 0 auto;
}

.mw-top .mw-len {
    flex: 0 0 auto;
}

/* ⚑ THE DURATION GIVES WAY FIRST AND DROPS OUTRIGHT (SAAS-421, Darius, 2026-10-02: "option (1), and
   yes"). Below a measured width the block does not draw its duration at all, and the practitioner
   takes the room.

   ⚠ KEYED TO THE BLOCK'S OWN WIDTH, NOT TO --mw-lane-min. The first proposal was a query on the lane
   keyed to that property. Print sets --mw-lane-min to 0 (SAAS-458) so seven panels divide the page,
   and the clipped glyph was found on paper — a rule keyed to the property would have stopped
   holding exactly there. The block is what is too narrow, so the block is the container, and the
   rule holds on screen and on paper alike. (It also sees a lane-sharing block, which is half a
   lane at any lane width.)

   ⚑ THE THRESHOLD IS WHAT THE TEXT MEASURES, not a number tried until it looked right. At 11px
   Inter: the time is 31.42px, a two-letter code 18.19, "drg. Sari" 43.50, "30m" 23.91, and three
   gaps are 4px each. A line that holds all four whole is therefore 31.42 + 18.19 + 43.50 + 23.91 +
   12 = 129.02px, and a block with less cannot show the duration without taking the room from the
   practitioner. 130 is that to the next whole pixel. It sits between the two cases this ticket has
   evidence for: the declared lane minimum leaves a block 129px of content, so the duration goes
   there; a printed block with a lane to itself at A4 landscape has 131.33px, so it keeps it.
   ⚠ BOTH MARGINS ARE ABOUT A PIXEL, deliberately: the ruling puts the line between two blocks that
   differ by 2.33px. A cancelled block has a 6px start border where a live one has 4, so it has two
   pixels less room and drops its duration where its live neighbour at A4 does not — real, and
   the honest answer for the narrower block it is.

   A four-letter code is wider (33.98px), so between 130 and 144.81px such a block draws the
   duration whole with its practitioner ellipsised, exactly SAAS-415's behaviour: the count of the
   clinicians not named is never what gives way (measured at the threshold with the widest duration,
   10h30 at 35.05px, 17.55px is left for a 3px gap and a 14.27px count).

   ⚠ NOT ON A PHONE'S SCREEN. Below 768px the practitioner is not drawn at all (the rule above, SAAS-415)
   and the phone's block keeps the time, the code and the duration, which fits at the lane minimum
   as it always did. A duration dropped there would buy room for nothing: it would take the length
   off every phone block, which nobody asked for. So the drop is for everything but a phone's
   screen — and paper is not a phone (SAAS-451), so it is matched there. `not screen and (...)` is
   that exactly: it negates the whole query.

   ⚑ A PHONE'S LANE-SHARING BLOCK HAS ITS OWN, NARROWER RULE — inside the phone's screen block, at the
   `@media screen and (max-width: 767px)` block, after the clinician's hide (the third SAAS-421
   build, Darius, 2026-10-03: "Fix"). Same
   container, same `display: none`, 82px where this one is 130 because a phone's line has no
   practitioner on it. A block with a lane to itself still keeps the duration there.

   `display: none` rather than a shrink: the duration stays in the markup, and the agenda row and the
   List carry the length as text. The block's title names it too: `MergedCalendarLook.Detail` says
   the length in words on every block ("30 minutes", or "no length recorded"). The third build found
   on 2026-10-03 that the title had named only a MISSING length, and Darius ruled the same day:
   "Yes, hover names it." */
@media not screen and (max-width: 767px) {
    @container mw-block (width < 130px) {
        .mw-top .mw-len {
            display: none;
        }
    }
}

/* ⚠ min-width: 0 IS THE LOAD-BEARING DECLARATION, exactly as it is on .cal-with, and for the same
   measured reason: a flex item's floor is its own content width until it is told otherwise, and this
   row has no overflow of its own to zero that automatically. Without it the name refuses to shrink
   and pushes the count clean past the block's edge, where the block's own overflow: hidden erases
   it — a booking shared between two clinicians reading as naming its only one. */
.mw-with {
    display: flex;
    flex: 0 1 auto;
    gap: 3px;
    align-items: baseline;
    min-width: 0;
}

/* The name gives way... */
.mw-with-name {
    flex: 0 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ...and the count of the clinicians NOT named does not. It is what stops a shared booking reading
   as naming its only clinician, so it is the last thing on the row that may shorten — and on this
   grid it never shortens at all. It inherits .mw-top's tabular figures. */
.mw-with-more {
    flex: 0 0 auto;
}

/* A booking the desk recorded no clinician against says so — italic, because it is a fact about what
   was recorded rather than about the appointment. It shortens with an ellipsis rather than being
   clipped mid-glyph. It used to be described as taking the no-length marker's manner from the item
   beside it; SAAS-421 took that marker off the block, so this one now carries the manner alone. */
.mw-without {
    flex: 0 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    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);
}

/* THE CLOCK AND NOTHING ELSE (SAAS-421). It was 8rem because it also held the length on a second
   line; the length moved to follow the practitioner, so this is a clock wide. */
.ma-time {
    flex: 0 0 4rem;
    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. It sits under the practitioner in `.ma-by` (SAAS-421),
   which is the order the block's line one reads in. */
.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;
}

/* WHO IS SEEING THE PATIENT, AND HERE IN FULL (SAAS-415). The grid beside this mode names one
   clinician and counts the rest, because its line one is four items wide at a 150px lane; this mode's
   whole rule is that nothing truncates, so the row lists every name. A fixed basis like its
   neighbours' — the clock is 4rem (SAAS-421; it was 8rem while it also held the length) and the
   clinic 5.5rem — because a ragged column is the thing a reader scanning a day has to re-find on
   every row; a list too long for it WRAPS rather than shortening, which is this mode's rule rather
   than a preference.

   THE CELL IS `.ma-by` SINCE SAAS-421 and holds the practitioner and, under it, the length. */
.ma-by {
    flex: 0 1 11rem;
    min-width: 0;
}

/* The cell's own basis moved to `.ma-by` (SAAS-421), which holds the practitioner and, under it, the
   length. The practitioner is a plain block inside it and still wraps rather than shortening. */
.ma-with {
    display: block;
    color: var(--muted);
    font-size: var(--t-small);
}

/* A booking the desk recorded nobody against, in the no-length marker's own manner one cell over. */
.ma-with.is-none {
    color: var(--ink);
    font-style: italic;
}

.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 --------------------------- */

/* Seven classes, one custom property. Every rule above paints with var(--branch), so a component
   never names a colour: a block, a legend entry and an agenda row all wear their clinic's identity
   the same way, and whether that identity is coloured is settled here and nowhere else.

   .tone-0 IS THE SEVENTH BRANCH AND BEYOND (SAAS-255): no hue, just ink. It is a class rather than
   an absent one so that nothing downstream has to special-case a missing colour — MergedCalendarLook
   .ToneClass answers "tone-0" for a Branch with no tone, and every rule painting var(--branch)
   keeps working unchanged. --muted rather than --ink because the identity is carried by the code
   beside it; the swatch and the block edge go grey with it, which is the honest reading. */
.tone-0 { --branch: var(--muted); }
.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);
    }
}

/* THE PHONE BREAKPOINT IS TWO BLOCKS, AND WHAT SEPARATES THEM IS THE MEDIA TYPE (SAAS-451).

   `@media (max-width: 767px)` names no media type, so it matches PAPER as well as a screen: a
   portrait A4 page is about 718px wide and a portrait Letter page 739px, both under 768. The rules
   in the block above are about the WIDTH — a lane that may be narrower before it scrolls, an agenda
   row that must stack before a name wraps — and a portrait page has that problem too, so they are
   left to reach it. Measured by printing the calendars at A4 portrait with the whole phone block
   moved under `screen`: the agenda went back to its date-column shape and wrapped every patient's
   name onto two lines, so the stacked agenda is what keeps a name whole on paper. The narrower
   lane was left in this block by SAAS-451 for a different reason: it decides how many lanes fit a
   page, and that ticket changed what a printed block says and nothing about how much of it fits.
   ⚠ THE 84px LANE DOES NOT REACH PAPER ANY MORE (SAAS-458, corrected by SAAS-467). The declaration
   above still sits in the unqualified block, which a printer matches, but `@media print` further
   down the sheet sets `--cal-lane-min` and `--mw-lane-min` to 0 and wins by coming later, and a printed
   week is landscape, so its seven panels divide the page (about 153px a day) and the name still
   ellipsises there (`drg. El…`). Only the stacked agenda and the rules around it are left on
   paper by this block. The one rule that is a decision about PHONES — taking the clinician off the
   block — is in the block below, which a printer does not match. */
@media screen and (max-width: 767px) {
    /* ⚑ A SCREEN RULE, AND IT STAYS ONE (SAAS-451). Unqualified, the drop below matched a portrait
       A4 or Letter page, and a calendar printed that way came off the printer with every block
       carrying the time and the patient and no doctor: read back out of the PDF, `no clinician` and
       the name after the time were absent from both grids. Paper is not a phone. The ruling below was
       made for a phone's width and for what a phone reader can do next — switch to the agenda, or to
       the List — and a sheet of paper has no hover to read the `title` from and no second mode to
       switch to, so the printed block is the only place the doctor can be. A printed calendar draws
       what the desktop one does.

       ⚑ AND THE CLINICIAN COMES OFF THE BLOCK HERE, BECAUSE THE PATIENT IS WHY THE BLOCK IS BEING
       READ — Darius, 2026-09-25. It replaces the wrap this same ticket shipped earlier that day,
       which gave the clinician its own line under the time.

       WHAT THE WRAP RESTED ON WAS NOT TRUE. It spent height, on the argument that a phone has
       height to spare. A time-axis calendar has none: a block's HEIGHT IS ITS DURATION, written
       inline in pixels by the layout engine, so the new line was not paid for out of spare room —
       it was paid for out of the patient's line. Measured at a 375px viewport against a
       byte-identical copy of this sheet: wrapped, a merged block at the 36px floor showed 2.50px of
       the patient's 13.75px line and a per-clinic block 7.72px of its 12.64px one, with the
       treatment gone from both. 77.6% of live bookings are drawn at that floor.

       SO THE PATIENT KEEPS ITS LINE AND THE CLINICIAN GOES. A block naming a doctor where the
       patient should be answers a question its reader did not ask. Dropped, each block is what it
       was before the clinician arrived — merged: the time, the clinic code, the duration, the
       patient; per-clinic: the time, the patient, the treatment — and it wants 50.25px and 41.92px
       again, which are the figures above this breakpoint.

       ⚠ THAT MERGED LIST SAID "THE LENGTH MARKER" UNTIL SAAS-421. Darius withdrew the marker on
       2026-09-25, so what a phone block keeps there is a duration where one was recorded and
       nothing where none was. THIS RULE IS UNCHANGED BY THAT: it hides the clinician and only the
       clinician, at this width and at no other.

       ONE RULE FOR BOTH GRIDS, AND THE SECOND ONE WAS MEASURED RATHER THAN ASSUMED TO MATCH. The
       per-clinic grid is the milder case and had to be established on its own: at its 84px lane the
       wrap left the patient 7.72px of 12.64 at the floor and still 0.42px short at 45 minutes, so
       it cannot hold the time, the clinician, the patient AND the treatment either. Two grids
       failing the same arithmetic get the same rule.

       WHERE A PHONE READER GETS THE DOCTOR INSTEAD. Each calendar has a second mode that carries
       the whole clinician list as text and truncates nothing: the merged week's agenda, which
       stacks at this very breakpoint — see .ma-day above — and the per-clinic calendar's List view,
       where the clinician is a column of the table. Each block's own `title` still carries the list
       as its description.

       ⚠ THE STATED ABSENCE GOES WITH THE NAME, deliberately. A block silent about the doctor EXCEPT
       where none was recorded would read as noise rather than as a record. A booking nobody was
       assigned to still says so above this breakpoint, in both second modes and in the hover, and
       what the stored column means is SAAS-416's to settle rather than this rule's. */
    .cal-block .cal-with,
    .cal-block .cal-without,
    .mw-with,
    .mw-without {
        display: none;
    }

    /* ⚑ A LANE-SHARING PHONE BLOCK DROPS ITS DURATION OUTRIGHT TOO (SAAS-421, third build — Darius,
       2026-10-03, on the one point the second build left for him: "Fix"). The 130px rule beside
       `.mw-top .mw-len` is gated OFF a phone, because a phone draws no practitioner and a block with a
       lane to itself holds its time, code and duration at 129px. It left the block that SHARES its
       lane clipping the duration mid-glyph: at 375, 414 and 767px the day column is 150px (a phone
       scrolls the merged week sideways rather than squeezing it) and a lane-sharing block has 54px of
       content, so it drew `09:00 WJ 3` — 8 of 8 in the fixture week. A duration clipped to a bare "3"
       is a wrong number, which is the fault the ticket exists to remove.

       SAME MECHANISM, SECOND NUMBER. The same block-keyed size container (`container: mw-block` on
       `.mw-block`, above), the same `display: none`, and a threshold measured from the line a phone
       block really draws, which has no practitioner and so one item and one gap fewer than the 130px
       line. Read off a real `.mw-block` at the phone's 11px Inter, after `document.fonts.ready`: the
       time 31.42px, a two-letter code 18.19, `30m` 23.91 and two gaps of 4px — 81.52px, and 82 is that
       to the next whole pixel. The second build estimated about 82; the measurement says the same.

       THE TWO CASES IT SEPARATES ARE FAR APART, so unlike the 130px rule it has margin: a lane-sharing
       phone block has 54px (the time, a gap and the code take 53.61, a duration needs 27.91 more) and
       drops it; a block with a lane to itself has 129px, or 127 where it is cancelled and its start
       border is 6px, and keeps it. A full-lane block holds even the widest code (four capital Ws,
       48.59px) and the widest duration (`10h30`, 35.05px) — 123.06px — so a duration drawn there is
       always whole.

       ⚠ A WIDTH RULE CANNOT SEE THE CODE, and that is stated rather than tuned away. A four-letter code
       is wider (33.98px), so a block between 82 and 97.31px would draw a duration it has no room for.
       The week's layout gives a phone block 129, 127 or about 54px and nothing between, so none arises
       there; the wide rule has the same limit between 130 and 144.81px.

       `display: none`, not a shrink, for the wide rule's reason: the duration stays in the markup, and
       the agenda row and the List carry the length as text (a phone has no hover; the block's title
       names the length for a screen reader — see the wide rule's note). Inside this block, which a printer
       does not match: paper keeps the 130px rule alone. Nothing else on a phone block is hidden by
       it. */
    @container mw-block (width < 82px) {
        .mw-top .mw-len {
            display: none;
        }
    }
}

/* --- 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 legacy repeated the
   whole item list under every payment (D5); this does not.

   ONE MEANING PER COLUMN (ADR 0025 addendum of 2026-09-06). What the treatment for these three row
   classes has to carry is that they are three DIFFERENT KINDS of row in one table: a bill is a band
   across it, a treatment fills two of its columns and a payment fills two others. Depth alone —
   which is what an indent gives — said "these belong to that" and never said "these are not the
   same sort of thing", and that is the distinction the page was failing to draw. */

/* ONE BLOCK PER BILL (ADR 0025 addendum of 2026-09-06, later — render 3B).

   THE SECOND PASS'S OWN COMPLAINT, in one sentence: "there should be a more obvious separator
   between receipts." A band was a row with a 2px rule and a wash, so two bills touched — no gap,
   no edge, nothing a scanning eye catches. A block is a bordered box with air around it, which is
   a separation the eye does not have to be told about. The six columns are unchanged and live
   inside it; what changed is the container. */
.portal-dayend .bill-blocks {
    display: flex;
    flex-direction: column;
    gap: var(--s4);
    margin-top: var(--s3);
}

.portal-dayend .bill-block {
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    overflow: hidden;
}

/* THE HEADER carries the bill's own facts — the ones that are about the bill and not about any row
   under it: the stamp, the identity, the badges, the total. It wraps rather than squeezes, and the
   total is held to the right edge by an auto margin, so a long patient name pushes the figure onto
   a second line instead of crushing it. */
.portal-dayend .bill-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s1) var(--s3);
    padding: var(--s2) var(--s3);
    background: var(--code-surface);
    border-bottom: 1px solid var(--line);
    font-weight: 600;
    color: var(--ink-strong);
}

/* The stamp. Tabular figures so a column of headers lines up down the page, which is the whole
   point of the date now being on it. */
.portal-dayend .bill-head .when {
    font-family: var(--mono);
    font-size: var(--t-micro);
    font-weight: 400;
    font-variant-numeric: tabular-nums;
    color: var(--muted);
    white-space: nowrap;
}

.portal-dayend .bill-head .num {
    margin-left: auto;
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* A VOIDED BILL'S BLOCK. --bad at 7%, the wash this product already uses to say "this row is the
   problem" — the same mix, from the same token, as the refusal rows elsewhere in the file. The
   badge inside it carries the word; this only makes the block findable in a day of forty. */
.portal-dayend .bill-block.voided .bill-head {
    background: color-mix(in srgb, var(--bad) 7%, transparent);
}

/* A BILL RAISED BEFORE THIS WINDOW. A left edge in the brand hue rather than a status colour:
   being older is not a fault, and --ok/--warn/--bad are the status ladder (ADR 0020). The edge is
   what makes the out-of-sequence block findable without claiming anything is wrong with it. */
.portal-dayend .bill-block.earlier {
    border-left: 3px solid color-mix(in srgb, var(--teal) 55%, transparent);
}

/* The sentence under an earlier bill's header, saying which figures it is not in. Muted and
   measured, like every other stated condition on this document. */
.portal-dayend .bill-block .says {
    margin: 0;
    padding: var(--s2) var(--s3) 0;
    font-size: var(--t-micro);
    color: var(--muted);
    max-width: var(--measure);
}

/* The block owns the frame, so the table inside it does not draw a second one. */
.portal-dayend .bill-block .tblbox {
    border: 0;
    border-radius: 0;
}

/* THE TREATMENTS keep their indent, because they still hang off the header above them. */
.portal-dayend tr.itemrow > td:nth-child(2) {
    padding-left: var(--s5);
}

/* THE PAYMENTS ARE TINTED APART, which is the ruling's own word for them. A faint teal, not a
   neutral: a second grey wash beside --code-surface would be two greys a reader has to tell apart,
   and the band's grey is the one that has to win. It is derived from --teal-wash rather than being
   a new colour, so it answers for both themes wherever that token does. The muted ink is the older
   half of the treatment and stays: money out is not the work. */
.portal-dayend tr.payrow > td {
    color: var(--muted);
    background: color-mix(in srgb, var(--teal-wash) 45%, transparent);
}

.portal-dayend tr.payrow > td:nth-child(4) {
    padding-left: var(--s5);
}

/* The reversal, and the sentence under a voided bill with no lines left. Full ink on a muted row,
   for the void marker's reason on the Billing ledger: the row is receding and the one word that
   changes what it means must not recede with it. */
.portal-dayend tr.payrow .reversal,
.portal-dayend tr.itemrow.void-note > td {
    color: var(--ink-strong);
}

.portal-dayend tr.itemrow.void-note > td {
    font-style: italic;
}

/* THE REMAINDER ROW — "Still owed — unpaid as of now" (ruling 1B). The warn wash, because an
   unsettled bill is a stated condition and --warn is this product's token for one; full ink on the
   figure, because it is the number the row exists to put in front of the reader. It closes the
   bill, so it takes a rule above it the way the fleet total does. */
.portal-dayend tr.remainrow > td {
    color: var(--ink-strong);
    font-weight: 600;
    background: color-mix(in srgb, var(--warn) 8%, transparent);
    border-top: 1px solid color-mix(in srgb, var(--warn) 35%, transparent);
}

/* Who raised the bill, on the header. It is the one fact there that is not the bill's own
   identity, so it takes the muted ink and the smaller size rather than the header's weight. */
.portal-dayend .bill-head .raised {
    font-weight: 400;
    font-size: var(--t-micro);
    color: var(--muted);
}

/* THE STATE BADGE — Paid, Voided, or "Owed 1,000,000". A pill rather than a word, because it is
   the one thing on the header a reader scans a column of, and colour is spent on status and on
   nothing else (ADR 0020). Three statuses, three tokens off the status ladder; the pill's own
   border carries the hue so the fill stays the header's. */
.portal-dayend .bill-head .badge {
    display: inline-block;
    padding: 1px var(--s2);
    border: 1px solid var(--border-strong);
    border-radius: 999px;
    font-size: var(--t-micro);
    font-weight: 600;
    color: var(--muted);
    white-space: nowrap;
}

.portal-dayend .bill-head .badge.ok {
    color: var(--ok);
    border-color: color-mix(in srgb, var(--ok) 45%, transparent);
}

.portal-dayend .bill-head .badge.owed {
    color: var(--warn);
    border-color: color-mix(in srgb, var(--warn) 45%, transparent);
}

.portal-dayend .bill-head .badge.void {
    color: var(--bad);
    border-color: color-mix(in srgb, var(--bad) 45%, transparent);
}

/* "Raised 2 Sep" — the mark on a bill from before the window. The brand hue, not a status one: it
   says where the bill came from, and nothing about whether anything is wrong. */
.portal-dayend .bill-head .badge.earlier {
    color: var(--teal-deep);
    border-color: color-mix(in srgb, var(--teal) 45%, transparent);
}

.portal-dayend .ref {
    font-family: var(--mono);
    font-size: var(--t-micro);
}

/* ------------------------------------------------- a doctor's four tiles */

/* They lead the block and the clinic's five follow, so the two ranks need telling apart at a
   glance — otherwise a doctor reads nine tiles as one row of figures and is back where the walk
   found them. The brand wash and a left rule do it: this rank is yours, the rank below is the
   clinic's. */
.portal-dayend .tiles.doctor-day {
    margin-top: var(--s3);
}

.portal-dayend .tiles.doctor-day .tile {
    background: var(--teal-wash);
    border-color: color-mix(in srgb, var(--teal) 30%, transparent);
}

.portal-dayend .tiles.doctor-day .tile .val {
    color: var(--nav-active-ink);
}

/* ------------------------------------------------- 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-menu,
    .shell-nav,
    .info-mark:not(.sync-mark),
    .practice-band-back,
    .portal-footer,
    .no-print {
        display: none !important;
    }

    /* THE PRACTICE BAND'S WAY BACK IS A BUTTON, so it does not print, on any page (SAAS-497, review 6). The
       band sits in the layout outside the masthead and the rail that this block drops, so it printed under
       every page's title, the dashboards' included (measured: "…their own rows on every page · elsa Switch to
       whole clinic" on a printed dashboard). Its SENTENCE stays: who the figures are about is state, and a
       sheet of paper that does not say it is the thing the band exists to prevent.

       ITS BOX SHRINKS TO THE SENTENCE. On screen the band is a full-bleed wash with 8 px of padding and a
       rule under it; paper drops the wash, and the padding and the rule were costing a calendar day the
       room it is zoomed to fit. Measured with the button gone: a practising Owner focused on a colleague,
       one booking at 10:00 on Melati's own day, still printed its closing count alone on a second sheet
       (the largest zoom that fits was 0.915, the shipped fit 0.925); with the box shrunk it is 0.935, so
       the day is one sheet. That is about 8 px in hand, and the fit does not count what sits above the
       grid, so a band that wraps to a second line puts the day back on two. (`body` is for specificity: the
       screen rule for the band is later in this sheet and would otherwise win.) */
    body .practice-band {
        padding: 0 0 var(--s1);
        border: 0;
    }

    /* THE CLINIC KEY PRINTS, IN FLOW (SAAS-496, SAAS-509). Behind a line on screen, but a printed week is read by
       its clinic codes and paper has no click — so the line prints as a heading and the list under it, whatever
       opened it, although the <details> around it is closed (SAAS-463's `::details-content` technique, behind its
       feature query: a browser without it prints the line alone). The picker does not print: no tick box, no
       Select all or Clear, no Show, no warning, no chevron, and no clinic that is not on the week.

       ONE LINE PER CLINIC, AND A NAME NEVER WRAPS (the coordinator's note on SAAS-509). The list was `columns: 3`:
       a third of the sheet each, so "Klinik Gigi Kenanga" and "Kidz Dental Puri Indah" broke in two in a cell that
       had room to spare. It is a wrapping row of cells now: every row is `nowrap`, and a line holds as many cells
       as the sheet allows. Each cell is as wide as the widest of them — the component writes that as
       `--mw-key-chars`, the longest name and count in characters — so the cells line up in columns; a cell that runs
       wider than the estimate simply takes what it needs, and one fewer fits a line. It never wraps and never
       overlaps. Where the cells fall in a column is the reader's to scan: the clinics are in name order, a line at
       a time. */
    /* EVERY OTHER CLINIC PICKER IS CHROME AND DOES NOT PRINT (SAAS-510): Billing's, Appointments' and the Dashboard's line, tick
       boxes, Select all, Clear, Show and ✕. A printed list is read by its rows (each names its clinic) and paper has no click.
       The calendar's key is the exception, by name: it wears `mw-legend`, and the rules below print its list. */
    .clinic-picker:not(.mw-legend) {
        display: none;
    }

    .clinic-picker-menu {
        display: block;
    }

    .clinic-picker-menu > summary {
        padding: 0;
        border: 0;
    }

    @supports selector(::details-content) {
        .clinic-picker-menu::details-content {
            content-visibility: visible;
            display: block;
        }
    }

    .clinic-picker-panel,
    .clinic-picker-scrolls .clinic-picker-panel {
        position: static;
        display: block;
        width: auto;
        min-width: 0;
        max-width: none;
        max-height: none;
        overflow: visible;
        padding: var(--s1) 0 0;
        border: 0;
        box-shadow: none;
    }

    .clinic-picker-group {
        display: block;
    }

    .clinic-picker-check,
    .clinic-picker-glyph,
    .clinic-picker-panel .select-all,
    .clinic-picker-panel .branch-none,
    .clinic-picker-panel legend,
    .clinic-picker-foot,
    .mw-legend-item.mw-legend-off {
        display: none;
    }

    .mw-legend-list,
    .clinic-picker-scrolls .clinic-picker-list {
        display: flex;
        flex-wrap: wrap;
        gap: var(--s1) var(--s4);
        overflow: visible;
    }

    .mw-legend-item {
        flex: 0 0 auto;
        min-width: calc(var(--mw-key-chars) * 0.56em + 4.6em);
        white-space: nowrap;
    }

    /* (Both selectors: a picker's row is `.clinic-picker-group .mw-legend-row` on screen, which is the more specific.) */
    .mw-legend-row,
    .clinic-picker-group .mw-legend-row {
        display: flex;
        align-items: baseline;
        gap: var(--s2);
        min-height: 0;
        padding: 0;
    }

    .mw-legend-row .mw-swatch {
        align-self: center;
    }

    .mw-legend-row .mw-code {
        min-width: 3.4em;
    }

    /* THE SYNC MARK'S SENTENCES PRINT, AND ITS BUTTON DOES NOT (SAAS-469). An explanation behind an
       information mark is dropped because paper reads like the screen, which no longer carries it;
       a warning about the copy is different — it qualifies the figures it sits beside, and a printed
       week or register that read as current while the copy was behind is the thing the notice was
       built to prevent. So the panel is drawn in flow, whatever opened it, with its heading and its
       sentences; the pill and its glyph are screen affordances. Darius may overturn. */
    .sync-mark {
        display: block;
    }

    .sync-mark > .info-mark-button {
        display: none;
    }

    .sync-mark .info-mark-panel {
        display: block !important;
        position: static;
        width: auto;
        max-width: none;
        max-height: none;
        overflow: visible;
        margin: 0 0 var(--s3);
        box-shadow: none;
    }

    /* 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;
    }

    /* A CLINIC THAT ONLY SAYS IT HAS NOTHING TO SHOW DOES NOT TAKE A SHEET TO SAY IT (SAAS-536). A doctor's Day End ends, for a clinic where they
       hold no rows, in a block of its summary line ("Kidz Dental Beta - nothing here is yours") and one sentence, and that block started its own sheet like
       the ones with bills in them: a last sheet of four lines (of one, in a capture whose blocks have not been opened, which is how review 8 found it).
       The sheet-per-branch rule is there so one clinic's pages can be handed to its manager, and a statement has none to hand. Such a block, and one
       that only says it could not be read (`.block-refusal`), follows the block before it where that sheet has room: A4 landscape 4 sheets to 3, F4
       3 to 2. And `break-inside: avoid`, because without it a block that did not quite fit split its sentence from its line (measured, A4 at 0.4 inch).
       Where the sheet before is full it prints whole on the next. ⚠ NOT `break-before: avoid`: it is no better (the block is still split where it straddles the foot, the
       same limit SAAS-536 measured on the lists, in their block further down), and it reads as a promise this sheet cannot keep. */
    .portal-dayend .dayend-block:has(> .block-body > :is(.block-own-rows, .block-refusal):only-child) {
        break-before: auto;
        break-inside: avoid;
    }

    .portal-dayend h2,
    .portal-dayend h3 {
        break-after: avoid;
    }

    /* A bill is one thing, so it does not straddle two sheets — and the border that says where it
       begins and ends is the whole of the second pass on paper. */
    .portal-dayend .bill-block {
        break-inside: avoid;
    }

    /* (5). The Billing detail's header, its tinted payment rows and its remainder row join the
       list: on paper they are the ONLY thing separating the kinds of row, and a printer that drops
       the washes prints the flat table the addendum replaced. */
    .portal-dayend tr.total > *,
    .portal-dayend tr.total.fleet > *,
    .portal-dayend .tile,
    .portal-dayend .bill-head,
    .portal-dayend .bill-block.voided .bill-head,
    .portal-dayend tr.payrow > td,
    .portal-dayend tr.remainrow > td,
    .portal-dayend .bill-head .badge,
    .portal-dayend thead th,
    .portal-dayend .empty-say {
        -webkit-print-color-adjust: exact;
        print-color-adjust: exact;
    }
}

/* ---------------------------------------------------------------------------
   A PRINTED CALENDAR WEEK FITS THE PAGE, AND THE PAGE IS LANDSCAPE (SAAS-458)

   It sits here, after the Day End's print block and before the last one, because several tests find
   a print block by its position in this sheet; the calendar's screen rules are far above.

   WHAT WAS WRONG. A week is seven day panels in a box that scrolls sideways on a screen
   (`.calendar { overflow: auto }`, `.cal-frame` and `.mw-frame` at `min-width: min-content`). Paper
   cannot scroll, so a printed week took the box's clip at the page's edge and a scrollbar with it:
   at A4 portrait the merged week lost its Sunday and half its Saturday, and the per-Branch week's
   second lane ran into Sunday's panel. Found by printing both through the print-proof harness that
   SAAS-451 built.

   WHY NOT LET IT SHRINK ON A PORTRAIT PAGE. That was tried first, and it does fit: no clip, all
   seven headings, text still 8.25pt. But a portrait page is 719px of content for a 46px ruler and
   seven days, about 99px a day, and a merged block spends its border, its padding, the time and the
   clinic code before the clinician is reached. Printed that way not one block of the merged week
   carried its clinician (`13:00 BAND 4`: the doctor and the length squeezed out), and the per-Branch
   week read `Tanpa ...` and `Ruang ...` for its patients. Leaving it to Chromium's own shrink-to-fit
   instead puts the legend at 6.2pt. So a week is printed LANDSCAPE: 1048px of content on A4, about
   153px a day, the column a desktop narrower than about 1100px gets, where SAAS-415 measured the
   clinician ellipsising (`drg. El…`) rather than going. A block that shares its lane with an
   overlapping booking is half that and still loses the name, as it does on a screen at that width.

   THE ORIENTATION IS ASKED FOR AND THE PAPER IS NOT. `size: landscape` turns whatever paper the
   printer holds on its side. No rule in this sheet names a paper size, and an A4 clinic should
   stay A4 and a Letter one stay Letter. The named page applies to a calendar page that is drawing a
   WEEK (more than one day panel) and to nothing else, so a day grid, the list and the agenda print
   as they did, and no other page in the product is turned on its side.

   WHERE IT IS GIVEN: TO `body`, NOT TO THE CALENDAR'S SECTION (SAAS-531). It was given to the section, and a
   reader focused on a colleague printed the week on THREE sheets: the Practice band's sentence ("Focused on
   drg. Elsa Puteri · their own rows on every page"), which sits in the layout OUTSIDE the section, alone on a
   portrait sheet 1, then the week on two landscape sheets (measured through `Page.printToPDF` on A4 portrait
   paper, in the real shell, on both the per-Branch week and the merged one). A named page changes at the
   section's edge, and a change of page name is a page break. SAAS-529 found the same fault on the lists and
   gave their page to `body`; this is the same, with the same reason. The `:has()` still asks for the SECOND day
   panel (SAAS-467), so a day, which prints portrait, never takes the name.

   THE FIT. No scroll box, no frame minimum, and the lane minimums, which are a screen's idea of how
   narrow a lane may get before it scrolls, are released so seven panels divide the page. A
   per-Branch day panel takes the width its lanes ask for rather than an equal seventh, so a Saturday
   with three clinicians is not squeezed to a third of what an empty Monday is given. The blank-day
   strip (`.mw-day.is-blank`) is unchanged, and the phone block's stacked agenda still reaches paper
   as SAAS-451 left it. */

@page calendar-week {
    size: landscape;
}

@media print {
    body:has(.portal-calendar .cal-days > .cal-day:nth-child(2)),
    body:has(.portal-calendar .mw-days > .mw-day:nth-child(2)) {
        page: calendar-week;
    }

    .portal-calendar {
        --cal-lane-min: 0px;
        --mw-lane-min: 0px;
    }

    .calendar {
        overflow: visible;
    }

    .cal-frame,
    .mw-frame {
        min-width: 0;
    }

    .cal-day {
        flex: 1 1 auto;
    }

    .mw-day {
        min-width: 0;
    }

    /* A PRINTED CALENDAR DROPS ITS CONTROLS (SAAS-497). Darius, 2026-10-09, on SAAS-482: "Hide
       controls, 0.8 floor." Paper can do nothing with a control, as the dashboards already rule
       ("chrome does not print"), and on a calendar the controls cost the grid its sheet: the CLINIC
       switcher and its line pushed a per-Branch day in a Tenant of several clinics onto a second
       sheet. Hidden on both calendars, every span and both renderings: the switcher and "One clinic at
       a time", and everything in the nav row but the date line. (The merged calendar's Advanced filter,
       `.branch-form`, was in this list; the page has none since SAAS-509, and the picker that took its place is
       the clinic key, which prints its list and none of its controls — the general print block above.)
       Kept: the title, the Copy synced line, the date line (`.cal-showing`), the merged clinic key
       (SAAS-496), the doctor's lead paragraph, the grid and the closing count.

       TWO MORE, from the fresh-eyes review of 2026-10-09 (review 6): the clinic key's ✕ (`.clinic-picker-clear`,
       SAAS-496), which printed as "16 bookings×Wed 5 Aug" on a day narrowed to 2 of 4 clinics, and the
       Focus on picker (`.practice-switch`: the label, the select, Go and the scope sentence), which a
       practising Owner focused on a colleague printed on both calendars. The dashboards already drop the
       picker on paper. The shell band's own button is hidden in the general print block above. */
    .portal-calendar .cal-branches,
    .portal-calendar .cal-one-clinic-line,
    .portal-calendar .clinic-picker-clear,
    .portal-calendar .practice-switch,
    .portal-calendar .cal-nav .cal-step,
    .portal-calendar .cal-nav .cal-today-link,
    .portal-calendar .cal-nav .cal-spans,
    .portal-calendar .cal-nav .cal-views,
    .portal-calendar .cal-nav .cal-axes {
        display: none;
    }

    /* A PRINTED DAY IS SCALED TO ONE SHEET (SAAS-480). Darius, 2026-10-03, shown the printed day
       spilling onto a second sheet once an hour became 72px: "Shrink the day to fit 1 sheet."

       THE GRID ZOOMS, THE PITCH DOES NOT DROP. `zoom` scales the whole grid card, so the 12px pitch,
       the 36px floor and the text all move together and two floored blocks 30 minutes apart still
       touch on paper. A smaller pitch under the same 36px floor would put them 27px apart and paint
       one over the other. (`transform: scale()` was not an option: it leaves the grid's layout height
       as it was and the page would still run to two sheets.)

       HOW FAR is not written here: AppointmentCalendarGrid.PrintFit writes it inline as
       `--cal-print-fit`, exactly enough to fit the grid in the room a sheet leaves under the heading
       (a short day is 1, unscaled), and never under 0.8 (6.6pt; SAAS-458 refused 6.2pt). Measured by
       printing: the largest zoom that fits one sheet is 0.944 for the default 09:00 to 21:00 window
       and 0.875 for a 13-hour one, and the shipped figures are 0.925 and 0.854. (Measured with the
       controls on the page. With them hidden, SAAS-497, the largest is 0.997 and 0.926; the shipped
       figures are kept.)

       A DAY ONLY: one day panel and nothing else. A week keeps its landscape sheets at full size
       (SAAS-458), and the screen never reads this.

       THE MERGED CALENDAR'S DAY TOO (SAAS-497): Darius, 2026-10-09, "Hide controls, 0.8 floor". Same
       mechanism, same floor: MergedWeekGrid writes `--cal-print-fit` from the merged day's own room,
       which is smaller, since the clinic key and the day's booking count sit above its grid. A day
       that cannot fit at 0.8 takes two sheets, and that is accepted. */
    @supports not ((container-type: size) and (width: calc(tan(atan2(1px, 1px)) * 1px))) {
        .portal-calendar:has(.cal-days > .cal-day:only-child) .calendar,
        .portal-calendar:has(.mw-days > .mw-day:only-child) .calendar {
            zoom: var(--cal-print-fit, 1);
        }
    }

    /* A DAY TOO TALL FOR ANY SHEET AT THE FLOOR (SAAS-513) is not laid out to fill one: it flows from sheet to sheet
       at the floor, the server's own fit (0.8 by construction), as every printed day did before. */
    .portal-calendar .cal-room.is-tall .calendar {
        zoom: var(--cal-print-fit, 1);
    }
}

/* ---------------------------------------------------------------------------
   A PRINTED CALENDAR IS ONE TITLE LINE, THE STATE, AND A GRID THAT TAKES THE REST OF THE SHEET (SAAS-513)

   Darius, 2026-10-09, on SAAS-497's open question (a long clinic key or a wrapped Focused band pushes a
   printed day onto a second sheet): "Count it, one sheet", and "also optimize the header, check whether we
   can fill some white space". Paper only; the screen is what it was.

   WHAT WAS WRONG, measured at A4 on f767fd3f. The day's fit ignored everything above the grid (a fixed room
   of 800 px, or 700 for the merged day), so a long key or a band that wrapped spilled to a second sheet; a
   normal day stopped 54 to 98 px short of the printable foot while its 30-minute blocks clipped their third
   line; about 330 px of a portrait sheet sat above the first hour of a Focused merged day; the date printed
   three times; and the shell's grey page background printed as a frame on every sheet.

   THE HEADING IS A TITLE LINE (below). THE DATE PRINTS ONCE: the h1, then the date, then the copy's sync line
   in small type, on one line; the date line in the controls, the day column's own date head and the closing
   count's date are dropped (`.cal-title-date` is a copy of the date line's words, because that line sits
   inside the controls and a title cannot reach into them). THE LANE HEADS STAY. EXPLANATION DOES NOT PRINT AND
   STATE DOES: the doctor's lead paragraph (what the page is) and the one-clinic offer (a link) go; the Focused
   band, the Narrowed sentence (its action link goes, in a `no-print` span), the practised-only lead, "N not
   read", the unread clinics' sentences and the sync sentences stay.

   THE FIT IS THE BROWSER'S, NOT THE SERVER'S AND NOT A SCRIPT'S (the choice the ticket asked to have said).
   How tall everything above and below the grid prints is not known on the server (a name wraps or it does not,
   a key is one row or four), and a script cannot measure it either: `beforeprint` runs on the screen's layout,
   and a `matchMedia('print')` listener runs on the print rules but at the WINDOW's width, not the paper's
   (measured in headless Edge: 756 px against a sheet of about 720: A4 at 1 cm margins is 721 through printToPDF), so a heading that wraps differently on paper is
   measured wrongly. What the browser lays out for the paper is the only true answer, so paper asks it: the day
   is a column that is exactly one sheet tall (`100vh` in print is the sheet's printable height, measured), the
   heading above and the count below take what they take, and the grid's room (`.cal-room`) is whatever is left.
   The room is a size container, so `.cal-fit` reads its height (`100cqh`) and divides it by the card's
   natural height (`--cal-grid-h`, written inline by the grid) to get `--cal-z`, in the print layout, with
   whatever paper, margins and heading the reader has. That is why the LAYOUT holds under a real Ctrl+P and
   not only in the harness. (One judgement in it was not made there and was wrong for other paper than A4 portrait: whether a day is "too tall"
   for any sheet, which the server decides against A4 portrait's 1,046 px. SAAS-525 below.)

   TWO DIRECTIONS FROM ONE NUMBER. `--cal-z` under 1 is a day that does not fit: the card is zoomed (text and
   all, so the 36 px floor and the pitch scale together and nothing paints over anything) to no less than 0.8,
   exactly as SAAS-480 ruled. `--cal-z` over 1 is a day with room to spare: the same `zoom` stretches the
   card, and every text size inside it is divided by the same number (the three tokens, below), so TEXT NEVER
   SCALES UP and only the pitch, the card's boxes and its lines grow: the hour rows reach the sheet's foot and
   a 30-minute block has the height for its three lines.

   A DAY THAT CANNOT FIT EVEN AT 0.8 goes to a second sheet, the floor held. Its room takes the height it
   needs (`min-height` of 0.8 times the card), and a size container is a box the browser does not cut across two
   sheets, so the grid goes WHOLE to the second sheet and the first holds the title line and the key. A day too
   tall for any sheet (`.is-tall`, which the server sets: 17 hours and up) is not laid out this way at all: it
   flows from sheet to sheet at the server's 0.8, as every printed day did before.

   AND A DAY TOO TALL FOR THE SHEET IT IS PRINTING ON (SAAS-525). `.is-tall` asks A4 portrait, whatever the paper. On A4 landscape or A5 (721 px
   of sheet) a day that A4 portrait holds is still a card the browser will not cut across two sheets, and it took the title alone on sheet 1, ran
   past its foot on sheet 2 and a third sheet held nothing, so the last hour never printed. The grid writes the one thing a media query
   cannot read, `@media print and (height < N)` with N the height of sheet its card needs at the floor with a title and a count
   (`AppointmentCalendarGrid.FlowsOnShorterSheets`), and under it two of the declarations below are overridden by their own selectors: the room
   is not a size container, and `--cal-z` is 0.8. The day then flows from sheet to sheet at the floor, as an `.is-tall` day does.

   FOR A BROWSER THAT CANNOT DO THE ARITHMETIC (no container size queries, or no `tan(atan2())`, which is
   Chromium 111 and 105) nothing above applies: the day flows as before and is zoomed by the SERVER's
   `--cal-print-fit`, the fixed room the old fit used (the fallback the ticket asked for). It still gets the
   title line, the white sheet and the single date.
   --------------------------------------------------------------------------- */

/* The ratio of two lengths is a number only through `atan2`: registered, so it is computed on `.cal-fit`
   (not zoomed) and reaches the zoomed card as a plain number rather than as lengths that would be scaled by
   the very zoom they decide. */
@property --cal-z {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@media print {
    /* THE SHEET IS WHITE; THE CARD KEEPS ITS BORDER. The shell's grey page wash printed as a frame round
       every sheet. A calendar's sheet only: the other printed documents are not this ticket's. */
    body:has(.portal-calendar) {
        background: var(--surface);
    }

    /* THE HEADING: A GRID OF ONE TITLE ROW, then whatever state there is, each line full width. The rows are
       placed rather than the markup moved, so the page's order (and a screen reader's) is untouched. */
    .portal-calendar {
        display: grid;
        grid-template-columns: auto auto minmax(0, 1fr);
        column-gap: var(--s2);
        align-content: start;
    }

    .portal-calendar > * {
        grid-column: 1 / -1;
    }

    /* The grid's room is a box on paper (it is nothing on a screen), so the card is a block inside a full-width
       item and not a lone item of the first column. */
    .portal-calendar > .cal-room {
        display: block;
    }

    /* The page's h1 at a print size (about 19 px, where the screen's is 27), then the date, then the sync
       line, all on the baseline. */
    .portal-calendar > h1 {
        display: block;
        max-width: none;
        grid-column: 1;
        grid-row: 1;
        align-self: baseline;
        margin: 0 0 var(--s1);
        font-size: 19px;
        line-height: 1.2;
    }

    .portal-calendar > .cal-title-date {
        display: block;
        grid-column: 2;
        grid-row: 1;
        align-self: baseline;
        margin: 0 0 var(--s1);
        color: var(--ink-strong);
        font-size: var(--t-body);
        font-weight: 600;
    }

    .portal-calendar > p.copy-synced {
        grid-column: 3;
        grid-row: 1;
        align-self: baseline;
        margin: 0 0 var(--s1);
        font-size: var(--t-micro);
    }

    /* The controls' own box (SAAS-497 hid everything in it but the date line and what is state) is one full-
       width item, so the state lines inside it flow as blocks. Its date line (`.cal-nav`) is the title's. */
    .portal-calendar > .cal-controls {
        display: block;
    }

    .portal-calendar .cal-nav {
        display: none;
    }

    /* EXPLANATION DOES NOT PRINT. The doctor's lead (what this page is: the same sentence's heading is the
       title) and the one-clinic offer (a link to another page); the practised-only lead is `.lead` too and is
       STATE, so it stays. The crumb is a link. */
    .portal-calendar .crumb,
    .portal-calendar .lead:not(.own-rows-only),
    .portal-calendar .cal-how-many {
        display: none;
    }

    /* STATE IS SMALL AND CLOSE. */
    .portal-calendar .narrowed,
    .portal-calendar .lead.own-rows-only,
    .portal-calendar .stated {
        max-width: none;
        margin: 0 0 var(--s1);
        font-size: var(--t-small);
    }

    /* THE MERGED NARROWED SENTENCE POINTS AT THE KEY ON PAPER (SAAS-507; Darius, 2026-10-15: "in the key below").
       "…at the clinics ticked below" is true of a screen, where the picker's boxes are below the sentence. On
       paper the picker's controls do not print (SAAS-509) and the clinic key does, so the sheet says "in the key
       below" and the screen keeps "ticked below" exactly. The markup carries a span for each; this block chooses,
       and it is a print block that is not the sheet's last one (BranchStaleMarkerTests and BranchPickerScrollTests
       read that one for something else). */
    .portal-calendar .narrowed .on-screen {
        display: none;
    }

    .portal-calendar .narrowed .on-paper {
        display: inline;
    }

    .portal-calendar .sync-mark .info-mark-panel {
        margin: 0 0 var(--s1);
    }

    .portal-calendar .cal-sites {
        margin-top: var(--s2);
    }

    /* THE CLOSING COUNT is the last line of the sheet, a caption under the grid. The date in it is a span that
       paper drops (the title line has it). */
    .portal-calendar > p.hint.count {
        margin: 2px 0 0;
        font-size: var(--t-micro);
        line-height: 1.3;
    }

    /* A DAY'S TITLE AND COUNT TAKE 35 PX BETWEEN THEM, NOT 43 (SAAS-535). Review 8 found the 16-hour per-Branch day, one sheet on Letter
       before SAAS-525, printing its grid on sheet 1 and its count alone on sheet 2. The card at the 0.8 floor is 944 px and Letter holds 981, which
       leaves 37 for the title row and the count; they took 43 (a 19 px title at 1.2 with 4 px under it, 26.8; an 11 px count at 1.3 with 2 px over it,
       16.3), so the day did not fit by 6 px, and had only ever been one sheet because the room's row ended early and drew the count over the
       card's last hour (SAAS-525, 4 above). A line of one size needs no more line box than its text: the title at 1.1 and 2 px under it is 22.9, the
       count at 1.1 with no margin is 12.1, and the date and the sync line beside the title, which set the row's height at their own 1.5, are set
       to match. Measured in Edge, print media at the printable width of Letter: the card begins at 22.9 and the count ends at 979.1 of the 981.3 a
       Letter sheet holds. A DAY ONLY: a week prints on its own landscape sheets and keeps its heading. The figure is
       `AppointmentCalendarGrid.PrintTitleAndCountPx`, and a test prints a day on a sheet a pixel either side of what that makes it need. */
    .portal-calendar:has(.cal-room.is-day) > h1 {
        margin-bottom: 2px;
        line-height: 1.1;
    }

    .portal-calendar:has(.cal-room.is-day) > .cal-title-date,
    .portal-calendar:has(.cal-room.is-day) > p.copy-synced {
        margin-bottom: 2px;
        line-height: 1.2;
    }

    .portal-calendar:has(.cal-room.is-day) > p.hint.count {
        margin-top: 0;
        line-height: 1.1;
    }

    /* PAPER HAS NO ELEVATION. The card's shadow is a dark wash of low opacity and, on a card pushed to the next sheet
       (a day too tall for the sheet it started on), a sliver of it is painted at the foot of the first. The card keeps
       its border. */
    .portal-calendar .calendar {
        box-shadow: none;
    }

    /* A DAY'S OWN DATE HEAD DOES NOT PRINT (a week's do: seven days are told apart by them). What the card
       keeps above its hours is the lane heads, so the ruler's blank corner is exactly as tall as one: a
       `--t-micro` line at 1.5 in 4 px of padding either side and a 1 px rule, the same sum the lane head is
       built from (`AppointmentCalendarGrid.PrintChromePx` carries it, with the card's two borders).
       (SAAS-539: the heads and the corner are the heading row's now, a table header that every sheet repeats, drawn from its own copy of them
       further down; the panels' own are hidden on paper, and the row is built from the same sizes, so the card is as tall as it was.) */
    .portal-calendar .cal-room.is-day .cal-date,
    .portal-calendar .cal-room.is-day .mw-date {
        display: none;
    }

    .portal-calendar .cal-room.is-day .cal-corner,
    .portal-calendar .cal-room.is-day .mw-corner {
        height: calc(var(--s1) * 2 + var(--t-micro) * 1.5 + 1px);
    }

    /* A 30-MINUTE BLOCK PRINTS ITS THREE LINES. A block is three lines of `--t-micro`: 3 x 12.65 px of text
       and 4 px of padding and border, 41.95 px, where a half hour is 36. The room the stretch gives is about
       10%, so the block's own chrome gives a little back as well: no padding above and below, and a line box
       of the text's own height (3 x 11 + 2 = 35 px, in the 36 a half hour is). The text is the same size.

       AND THE LETTERS OF THE THIRD LINE (SAAS-525). SAAS-513 took the line box to 1.1 (3 x 12.1 + 2 = 38.3 px), which fitted a block only when
       the card was stretched by 6.4% or more, and the merged day is stretched by 1.025 and 0.923 (and a day that flows is 0.8): its blocks are
       36 px at every one of them, so the third line had 33.9 px of text-and-border to stand in and the browser cut its foot. "Scaling gigi"
       printed as "Scalina aiai". Line-height 1 holds three lines in the 34 px inside the borders, at any zoom of 1 or under (and the text is
       divided by a stretch over 1, so it fits there with room). */
    .portal-calendar .cal-room.is-day .cal-block {
        padding-block: 0;
        line-height: 1;
    }

    .portal-calendar .cal-room.is-day .mw-block {
        padding-block: 0;
        line-height: 1;
    }

    /* THE CLIP OF A LINE REACHES UNDER ITS BASELINE (SAAS-525). The patient and the treatment each clip at their own box, for the ellipsis and
       so a long name cannot spill, and the box is the line: with a line box of 11 px the baseline is 1.5 px above its foot, where a g, p, y or q
       goes 2.2 px (Inter, measured at 16 times). A quarter of an em of padding under the line puts the clip where the letter ends, and the
       same under a margin takes it back, so nothing in the block moves. */
    .portal-calendar .cal-room.is-day .cal-who,
    .portal-calendar .cal-room.is-day .cal-what,
    .portal-calendar .cal-room.is-day .mw-who,
    .portal-calendar .cal-room.is-day .mw-what {
        padding-bottom: .25em;
        margin-bottom: -.25em;
    }

    .portal-calendar .cal-room.is-day .mw-who {
        margin-top: 0;
    }

    /* EVERY LINE OF A BLOCK IS WHOLE, IN A WEEK AND ON THE FIRST LINE TOO (SAAS-537). Fresh-eyes review 8 found two places SAAS-525's care did not reach, and
       both were already on `9bca2030`. (1) THE FIRST LINE: "drg. Di..." is a flex item (`.cal-with-name`, `.mw-with-name`, and `.cal-without`, `.mw-without` for a
       booking no clinician was recorded against) that clips at its own box for the ellipsis, and its box is the line, so on the merged day at a zoom of 0.8 or a
       per-Branch day on A4 landscape the "g" of "drg." lost its foot: "drq." (baseline 182.00, the g's ink to 183.90, the clip at 183.00). The same quarter of an
       em under the line, taken back under a margin, as the patient and the treatment have it (above); a day or a week, every block. (2) A WEEK'S HALF-HOUR BLOCK
       is 36 px at every zoom (a week is not zoomed) for three lines, and the base line box was 1.15 on the per-Branch grid (3 x 12.65 + 2 + 2 = 41.95 px) and 1.25
       on the merged (50 px): the third line showed its top half, or nothing. The block's own chrome gives back what a day's does (no padding above or below, a
       line box of the text's own height: 3 x 11 + 2 = 35 px in the 36). Measured, the rest of a day's care is not needed here: with the patient's and the
       treatment's clips left at their own boxes and the merged patient's margin kept, no letter of either is cut (planted, the harness reads every line whole), so
       a week gets the two rules above and no more. A line that cannot fit whole is not drawn: at line-height 1 a block holds three lines at any zoom of 1 or
       under, so no line is left to drop; a day taller than that is stretched, and its text is divided by the stretch. */
    .portal-calendar :is(.cal-with-name, .cal-without, .mw-with-name, .mw-without) {
        padding-bottom: .25em;
        margin-bottom: -.25em;
    }

    .portal-calendar .cal-room:not(.is-day) :is(.cal-block, .mw-block) {
        padding-block: 0;
        line-height: 1;
    }
}

/* ---------------------------------------------------------------------------
   THE HOUR LINES ARE BORDERS ON PAPER (SAAS-525), WHATEVER THE PRINT DIALOG SAYS ABOUT BACKGROUNDS

   Review 7 (2026-10-09): the hour lines of both calendars were a `repeating-linear-gradient`, and so were the block fills, and a
   gradient is a background. "Background graphics" is off in Chrome's and Edge's Ctrl+P by default, so a default print lost every hour
   line, and a block was its border on a blank sheet. (A viewer that draws a gradient from its shading function also drew grey bands at
   some hours where Edge's own drew hairlines; the PDF holds no line to read.)

   EACH HOUR HAS ITS OWN ELEMENT NOW, `.cal-rules > div` and `.mw-rules > div`, one per hour of the window and as tall as the hour is
   (the grids write them, ahead of the lanes, so a block is drawn over its line), and the line is that element's `border-top`. A border
   prints whatever the dialog says, it is in the PDF as a line of its own, and it is the same colour the gradient was. The screen is
   unchanged: the elements are `display: none` there, and paper stops drawing the gradient under them so a line is drawn once.
   A BLOCK STAYS READABLE WITHOUT ITS FILL: it keeps its border and its coloured edge, which are borders too, and the hairline behind its
   text is the page's own light grey.
   --------------------------------------------------------------------------- */
@media print {
    .cal-body,
    .mw-body {
        background-image: none;
    }

    .cal-rules,
    .mw-rules {
        display: block;
        position: absolute;
        inset: 0;
        pointer-events: none;
    }

    .cal-rules > div,
    .mw-rules > div {
        box-sizing: border-box;
        border-top: 1px solid var(--border);
    }
}

/* ---------------------------------------------------------------------------
   THE HEADING ROW REPEATS ON EVERY SHEET, AND THE FRAME IS ONE GRID OF ONE-PIXEL ROWS (SAAS-539)

   Darius, 2026-10-10, on SAAS-525's disclosed decision (a), which was that a lane heading does not repeat: "Repeat headings". Every printed sheet
   after the first says which day a column is (a week: the date and, on the merged week, the day's count) or which clinician or room a lane is (a
   day), by printing the heading row again at its top.

   WHAT WAS MEASURED, in headless Edge through `Page.printToPDF` (the path Ctrl+P takes), reading every text run and every line off the PDF:

   1. ONLY A REAL `<thead>` IS PRINTED AGAIN. `display: table-header-group` on a `div` (inside a `display: table` div, or inside a `<table>`) did not
      repeat on any sheet, nor did `position: fixed` (it is on every sheet, sheet 1 included, and cannot leave room for itself on later sheets only),
      nor a cloned padding (`box-decoration-break: clone`). So the grid is inside a `<table>` and the heading row is its `<thead>` (the markup:
      AppointmentCalendarGrid, MergedWeekGrid), drawn once more for paper in a copy the screen hides (`.cal-ph`) beside the panels' own, which paper hides.
   2. AN ABSOLUTELY PLACED BOX IS NOT MOVED DOWN BY THE REPEATED HEADER. The grid placed its blocks (and its hour lines, in a box of their own) with
      `position: absolute`. In a table with a repeated header the placed content of every later sheet was drawn H px too high (H the header's
      height: 40 px on the probe), so its first H px of rows were drawn UNDER the header, where nobody can read a booking, and it drifted against the
      hour ruler, which is in the flow. That is the other half of "repeat headings", and it is why this is not a `<thead>` and a change of nothing else.
   3. SO NOTHING IS ABSOLUTELY PLACED ON PAPER: the frame is one `display: grid` of slot rows (12 px, 72 to the 12-hour day, and a last row for what is left
      of it), the ruler's hours, the hour lines and every block are grid items placed by row (`--gr`, `--gn`, and `--go`, how far under its row's top a
      block starts, written inline by the grids beside the `top` and `height` a screen reads, so a screen is untouched), and a lane is a column. Measured,
      a grid like this keeps every booking on its own hour on every sheet (0.0 px apart from the ruler across three sheets), where one grid per lane
      inside a flex row drifted by up to 9 px a sheet. WHY NOT A ROW A PIXEL, which would place a block with no offset: a browser sizes a zoomed grid's rows
      in sixty-fourths of a pixel and cuts the rest, so 864 rows of 1.1347 px were each 1.125 and a printed day came out 8 px short of its sheet (a day
      that is stretched fills its sheet to the pixel, and a test holds it to a few). 72 rows of 13.6 lose about a pixel.
   4. ONE GRID, NOT ONE PER DAY. A grid breaks between its rows, and an item that does not fit (a block whose first line is under the foot) is pushed
      whole to the next sheet and takes the break with it. A day column of its own would break at its own place: a block that starts a few pixels over
      the foot moved ITS day's rest of the grid up by those pixels, and the ruler and every other day stayed. The ruler and every day's columns are in
      the one grid, so the whole frame breaks in one place, between two rows, and the hour lines stay level across the week. (A sheet may therefore stop
      up to a slot short of its foot. A block that starts higher and runs past the foot is cut where the sheet ends, and the rest of it continues at the
      top of the next sheet, below the heading row.)
   5. THE TWO GRIDS SHARE ONE LIST OF COLUMNS (`--cal-cols`): a first column of the ruler's own width, then one `minmax(0, 1fr)` for every lane, the
      days with nothing booked on a merged week the strip's width (`--mw-blank`). That is a printed week's width by lanes and no longer by what a day's
      names are wide, as a screen divides it (SAAS-514).

   A SCREEN IS UNCHANGED: the table's parts draw no box there (`display: contents`) and the heading copy is `display: none`.
   --------------------------------------------------------------------------- */
.cal-t,
.cal-t > thead,
.cal-t > tbody,
.cal-t tr,
.cal-t th,
.cal-t td {
    display: contents;
}

.cal-ph {
    display: none;
}

/* THE WEEK'S CLOSING COUNT, DRAWN ONCE MORE INSIDE THE FRAME FOR PAPER (SAAS-560): a screen has the page's own, under the card. */
.cal-close {
    display: none;
}

@media print {
    .cal-t {
        display: table;
        width: 100%;
        table-layout: fixed;
        border-collapse: collapse;
        border-spacing: 0;
    }

    .cal-t > thead {
        display: table-header-group;
    }

    .cal-t > tbody {
        display: table-row-group;
    }

    .cal-t tr {
        display: table-row;
    }

    .cal-t th,
    .cal-t td {
        display: table-cell;
        padding: 0;
        border: 0;
        font: inherit;
        text-align: inherit;
        vertical-align: top;
    }

    /* THE HEADING ROW, AS THE PANELS' OWN HEADS DREW IT (the same sizes, so a card is as tall as it was): a day's date, and under it the lane heads
       (a per-Branch grid) or the day's count (the merged week), with the rule under it. Its columns are the frame's. */
    .cal-ph {
        display: grid;
        grid-template-columns: var(--cal-cols);
        break-inside: avoid;
    }

    .ph-corner {
        border-inline-end: 1px solid var(--border);
    }

    .ph-day {
        grid-column: span var(--cal-lanes, 1);
        min-width: 0;
        border-inline-end: 1px solid var(--border);
    }

    .ph-day:last-child {
        border-inline-end: 0;
    }

    .ph-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;
    }

    .calendar.mw .ph-date {
        padding: var(--s2) var(--s2) 0;
        line-height: 1.5;
    }

    .ph-day.is-blank .ph-date {
        color: var(--muted);
        font-weight: 600;
    }

    .ph-day.is-today .ph-date {
        color: var(--action);
    }

    .ph-today {
        margin-inline-start: var(--s1);
    }

    .ph-lanes {
        display: flex;
        border-bottom: 1px solid var(--border-strong);
    }

    .ph-lane {
        flex: 1 1 0;
        min-width: 0;
        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;
    }

    .ph-lane:first-child {
        border-inline-start: 0;
    }

    .ph-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;
    }

    /* A day's own date does not print on a day (the title line has it), and what is left of the heading row there is the lane heads. */
    .portal-calendar .cal-room.is-day .ph-date {
        display: none;
    }

    /* THE PANELS' OWN HEADS ARE THE HEADING ROW'S NOW, and the ruler's blank corner is the row's first cell. */
    .cal-frame .cal-corner,
    .cal-frame .cal-date,
    .cal-frame .cal-lane-heads,
    .mw-frame .mw-corner,
    .mw-frame .mw-date,
    .mw-frame .mw-count {
        display: none;
    }

    /* THE FRAME IS ONE GRID: the ruler, every day and every lane are columns of it, and every hour, hour line, lane rule and block an item placed by row.
       The boxes between the frame and its items draw nothing on paper (`display: contents`); a lane's rule and a day's right edge are drawn by the items
       below, which is what a lane's and a day's own borders were. */
    .cal-frame,
    .mw-frame {
        display: grid;
        grid-template-columns: var(--cal-cols);
        grid-template-rows: repeat(var(--gfull), var(--gslot)) var(--gtail) auto;
        align-items: stretch;
    }

    .cal-frame .cal-gutter,
    .cal-frame .cal-hours,
    .cal-frame .cal-days,
    .cal-frame .cal-day,
    .cal-frame .cal-body,
    .cal-frame .cal-rules,
    .cal-frame .cal-lane,
    .mw-frame .mw-gutter,
    .mw-frame .mw-gutter > div:not(.mw-corner),
    .mw-frame .mw-days,
    .mw-frame .mw-day,
    .mw-frame .mw-body,
    .mw-frame .mw-rules,
    .mw-frame .mw-lane {
        display: contents;
    }

    .cal-frame .cal-hour,
    .mw-frame .mw-hour {
        grid-column: 1;
        grid-row: var(--gr) / span var(--hrows);
        border-inline-end: 1px solid var(--border);
    }

    .cal-frame .cal-rules > div,
    .mw-frame .mw-rules > div {
        grid-column: var(--gc) / span var(--cal-lanes, 1);
        grid-row: var(--gr) / span var(--hrows);
        border-inline-end: 1px solid var(--border);
    }

    .cal-frame .cal-day:last-child .cal-rules > div,
    .mw-frame .mw-day:last-child .mw-rules > div {
        border-inline-end: 0;
    }

    .cal-frame .cal-lane::before {
        content: "";
        grid-column: var(--gc);
        grid-row: 1 / -2;
        border-inline-start: 1px solid var(--border);
    }

    .cal-frame .cal-rules + .cal-lane::before {
        display: none;
    }

    /* A BLOCK STANDS WHERE ITS ROWS ARE, in its lane's column: the `top` a screen reads is taken off, the `height`, `left` and `width` stay (the left
       and the width are shares of the column the block is placed in, which is what they were of its lane). A block may break between its lines.
       A block, or a "+N more", is the grid's own item: it stays above the hour lines because it follows them in the markup. */
    .cal-frame :is(.cal-block, .cal-more),
    .mw-frame :is(.mw-block, .mw-more) {
        position: relative;
        top: auto !important;
        grid-column: var(--gc);
        grid-row: var(--gr) / span var(--gn, 1);
        align-self: start;
        margin-top: var(--go, 0px);
        orphans: 1;
        widows: 1;
    }

    .cal-frame .cal-more,
    .mw-frame .mw-more {
        justify-self: end;
        align-self: start;
    }

    /* A DAY WITH NOTHING BOOKED says so at the top of its column, under the heading row. */
    .cal-frame .cal-empty,
    .mw-frame .mw-none {
        grid-column: var(--gc);
        grid-row: 1 / span 6;
        align-self: start;
    }

    /* A WEEK'S CLOSING COUNT IS THE FRAME'S LAST ROW, AND THE LAST HOUR AND THAT ROW ARE ONE ITEM THAT IS NOT CUT (SAAS-560). Fresh-eyes review 9 (reviewer C): a
       week is never zoomed, so SAAS-535's stretch is not available to it, and the heading row that SAAS-539 repeats takes 25 to 52 px of every later sheet. The
       last sheet of the grid ended at its foot, the count under the card (about 16 px) did not fit, and it printed alone on a sheet of its own with no heading and
       no grid: the per-Branch week at 06:00 to 22:00 on A4 at 0.75 inch (2 sheets at `3eee583b`, 3 at `bc559423`) and the per-Branch week on A5 (3 sheets, the count
       alone on the third). `break-before: avoid` on the page's count, in the page's own grid, was measured and changed nothing: that row is in another grid than the
       frame's, and a browser breaks the page's rows where it likes. So the count is a row of THE FRAME'S grid (the grids draw a copy of it, as the heading row is
       copied for the thead), and a box the width of the frame that starts at the last hour's own first row (`--glast`, written by the grids) and runs to the end of the
       grid, with `break-inside: avoid`, is what a browser cannot cut: where the last sheet would end inside it, all of it goes to the next sheet, with the heading
       row that repeats there. The last sheet then holds the last hour of the grid and the count under it, never the count alone. (Measured on the sheets that failed:
       21:00 and the count on the third sheet.) The box starts at the hour's own row and not an hour's rows from the end, because a grid can be taller than its ruler
       (the merged week at 06:00 to 22:00 is 97 rows, a floored late booking runs past 22:00): started a row late, the box left the label "21:00" alone on one sheet
       with the rest of its hour on the next. The copy is inside the card, under the last hour, where the page's own count is under the card, and its row is as tall as
       the page's own count was (a line at 1.3 and 2 px over it), because the merged week on A5 had room for that and not for a pixel more. The page's own is hidden for a
       week (a day has no copy, and is stretched instead). */
    .cal-frame .cal-close,
    .mw-frame .cal-close {
        display: block;
        grid-column: 1 / -1;
        grid-row: -2 / -1;
        margin: 0;
        padding: 2px var(--s2) 0;
        color: var(--muted);
        font-size: var(--t-micro);
        line-height: 1.3;
    }

    .cal-frame:has(> .cal-close)::after,
    .mw-frame:has(> .cal-close)::after {
        content: "";
        grid-column: 1 / -1;
        grid-row: var(--glast) / -1;
        break-inside: avoid;
    }

    .portal-calendar:has(.cal-close) > p.hint.count {
        display: none;
    }
}

/* THE DAY THAT TAKES THE REST OF ITS SHEET, where the browser can work the room out (see the block above).
   `container-type: size` is Chromium 105, `tan(atan2())` is 111. */
@supports (container-type: size) and (width: calc(tan(atan2(1px, 1px)) * 1px)) {
    @media print {
        /* THE SHEET IS ONE COLUMN, EXACTLY ONE SHEET TALL: the body (the band above, the shell's frame, the
           page) down to the section, each taking what is left of the one above. `100vh` in print is the
           sheet's printable height (measured at A4: 1,046 of 1,047 px), and it is a definite height, which
           the grid's own room needs before its container can be read. */
        body:has(.portal-calendar .cal-room.is-day:not(.is-tall)) {
            display: flex;
            flex-direction: column;
            height: 100vh;
        }

        body:has(.portal-calendar .cal-room.is-day:not(.is-tall)) > .shell-body,
        body:has(.portal-calendar .cal-room.is-day:not(.is-tall)) .shell-body > .site-main {
            display: flex;
            flex-direction: column;
            flex: 1 1 0;
            min-height: 0;
        }

        /* The section's rows: the title row and the state above the grid are 20 rows that cost nothing when
           empty, then the grid's room takes the rest, then the closing count. (Items are placed in order into
           the first free rows, so the clinics' roll call, which follows the count in the markup, lands above
           the grid as state does.)

           THE ROOM'S ROW IS NEVER SHORTER THAN THE ROOM (SAAS-525). It was `minmax(0, 1fr)`: the rest of the sheet, from nothing. A room
           has a `min-height` of 0.8 of its card, and where that is more than the rest of the sheet (a day that fits a sheet by itself but not
           under its heading, which goes whole to the second) the row stayed the rest and the room overflowed it, so the count's row began
           where the TRACK ended and was drawn over the card's last hour: 25 px up on a merged 16-hour day under its key on A4 portrait. `auto`
           as the track's minimum is the room's own min-height, and the row is still the rest of the sheet wherever that is more. */
        .portal-calendar:has(.cal-room.is-day:not(.is-tall)) {
            flex: 1 1 0;
            min-height: 0;
            grid-template-rows: repeat(20, auto) minmax(auto, 1fr) auto;
        }

        .portal-calendar:has(.cal-room.is-day:not(.is-tall)) > p.hint.count {
            grid-row: 22;
        }

        .portal-calendar .cal-room.is-day:not(.is-tall) {
            display: block;
            grid-row: 21;
            container-type: size;
            min-height: calc(var(--cal-grid-h) * 0.8);
        }

        .portal-calendar .cal-room.is-day:not(.is-tall) > .cal-fit {
            display: block;
            --cal-z: tan(atan2(100cqh, var(--cal-grid-h)));
        }

        /* Zoom to fit; stretch to fill. The zoom is `--cal-z`, never under the 0.8 floor. Over 1 the same
           zoom stretches the card and the three text tokens are divided by it, so text is the size it was
           (the literals are the tokens' own values in `:root`; a test holds them equal). */
        .portal-calendar .cal-room.is-day:not(.is-tall) .calendar {
            zoom: max(0.8, var(--cal-z));
            --t-micro: calc(11px / max(1, var(--cal-z)));
            --t-small: calc(12.5px / max(1, var(--cal-z)));
            --t-body: calc(14px / max(1, var(--cal-z)));
        }
    }
}

/* ---------------------------------------------------------------------------
   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);
}

/* TWO SELECTORS PER RULE, ONE CONTROL (SAAS-259). The Operator editor's Scope field is the same
   dual list as a Group's Granted Reports — Darius ruled shape B for it on 2026-09-08 exactly so
   that there would be one idiom for "pick a subset of a long list" rather than two — but it sits
   on a page instead of in a dialog, and every rule below was written under `.form-dialog`. The
   second selector is what keeps them one control: a change to the grid, the arrows or the stacking
   breakpoint reaches both, and neither can quietly drift.

   `.operator-editor` rather than a bare `.dual`: that realm's own `input`/`select` rules give every
   control a width and a border, and a two-class selector is what outranks them. What the editor
   still owes on top — undoing the 42rem cap — is in operator.css beside the field.

   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,
.operator-editor .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,
.operator-editor .dual .col {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.form-dialog .dual .col h4,
.operator-editor .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,
.operator-editor .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,
.operator-editor .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,
.operator-editor .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,
.operator-editor .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,
.operator-editor .dual option.custom {
    color: var(--scope-custom, var(--ink));
}

.form-dialog .dual .btns,
.operator-editor .dual .btns {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: var(--s2);
}

.form-dialog .dual .btns .move,
.operator-editor .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,
.operator-editor .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,
.operator-editor .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).

   ⚑ A SCREEN RULE (SAAS-467). Unqualified, it also matched a printed A4 or Letter page and a dialog
   printed while open stacked the lists, which put the Granted list wholly under the dialog's own cut
   where side by side shows the top of both. Paper is not a phone. */
@media screen and (max-width: 56.25rem) {
    .form-dialog .dual,
    .operator-editor .dual {
        grid-template-columns: 1fr;
        margin-left: 0;
    }

    .form-dialog .dual .btns,
    .operator-editor .dual .btns {
        flex-direction: row;
        justify-content: center;
    }

    .form-dialog .dual .btns .move .across,
    .operator-editor .dual .btns .move .across {
        display: none;
    }

    .form-dialog .dual .btns .move .down,
    .operator-editor .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;
}

/* ---------------------------------------------------------------------------
   THE GROUP HEADING (SAAS-381) — ruled by Darius on 2026-09-19 as shape B4, amended in the same
   breath: "can we just sum the total in the header instead, and make it the total of all filtered
   data instead of just that page?"

   It is drawn ONLY on a grouped reading, which since SAAS-379 means the reader chose the
   receipt-number column. The default date reading has no headings and keeps its Clinic column.

   IT IS A BAND RATHER THAN A ROW IN BOLD. A heading that reads as an ordinary row is how a reader
   comes to take a clinic's total for one bill — these figures sit directly above bills, in the very
   same three columns, and what keeps the two apart on screen is this treatment: a heavier rule
   above, a wash behind, and the clinic's name at a weight no row carries. The wash is --row-hover,
   the one wash this table already has in both themes, rather than a new token nobody has measured.

   ⚠ AND IT CARRIES ITS OWN SCOPE IN WORDS. The figures are of the WHOLE FILTERED READING and the
   rows beneath are one page of it, so adding the visible rows gives a different number. That is the
   ruling and it is right; the qualifier beside the clinic's name is what stops it reading as an
   error. Set small and muted because it explains rather than heads, and allowed to wrap so a long
   clinic name takes it along instead of pushing the money off the scroller. */
table.ledger tr.group-head > th,
table.ledger tr.group-head > td {
    background: var(--row-hover);
    border-top: 2px solid var(--border-strong);
    border-bottom: 1px solid var(--line);
    padding-block: var(--s2);
    vertical-align: baseline;
}

table.ledger tr.group-head th.group-name {
    text-align: left;
    font-weight: 600;
}

table.ledger tr.group-head .group-clinic {
    color: var(--ink-strong);
    font-size: var(--t-body);
    font-weight: 700;
    margin-inline-end: var(--s3);
}

table.ledger tr.group-head .group-scope {
    color: var(--muted);
    font-size: var(--t-small);
    font-weight: 400;
    white-space: normal;
}

/* The three figures, in the table's own tabular numerals and one step stronger than a row's,
   because they are the summary of everything under them. The balance keeps the row cells' two
   states — an owed total is the one figure on this band a reader acts on. */
table.ledger tr.group-head td.num {
    color: var(--ink-strong);
    font-weight: 700;
}

/* A clinic that owes nothing across the whole filter recedes, exactly as a settled row's balance
   does — the same two renderings of one fact, one level up. Stated at a higher specificity than the
   rule above it rather than left to the cascade, because `td.num` and `td.balance.settled` are
   otherwise a tie broken by element count. */
table.ledger tr.group-head td.balance.settled {
    color: var(--muted);
}

/* 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;
    }

    /* ⚑ 1a. THE ONE EXCEPTION TO THE FLOOR, WRITTEN WHERE THE FLOOR IS DEFINED — 2026-09-19,
       SAAS-378, and it is the only one: one reason, now worn by the two calendar blocks that are
       anchors (the per-clinic block's twin is the next rule, SAAS-449). An unexplained exception to
       an accessibility rule is how the rule dies, so here is the whole reason.

       A CALENDAR BLOCK'S HEIGHT IS NOT A LOOK, IT IS A FACT. `Placement()` in MergedWeekGrid emits
       `top:{TopPx};height:{HeightPx}`: the top IS the booking's start time and the height IS its
       duration, both in pixels off the same axis the hour ruler is drawn on. Darius's ruling made
       the whole block the anchor that opens the patient, so the floor above now reaches it — and a
       block floored at 44px draws a 30-minute booking as if it ran 45 minutes, over the top of the
       booking after it. Measured in the harness at a real 375px lane: 36px becomes 44px and the
       09:00 half-hour block crosses the 09:30 rule. A calendar that lies about duration is a worse
       product than one whose blocks are 36px tall.

       WHAT IS GIVEN UP, SAID PLAINLY: a half-hour booking is a 36px target rather than a 44px one,
       8px short on the shortest blocks only. What was bought: every block of 50 minutes or more
       clears the floor by its own geometry (72px for an hour), and the target is a whole block
       rather than the 16px word inside it that option B of the ticket would have exempted. The
       floor is not carved out for a span anywhere in this portal, and it still is not.

       ⚠ `.mw-more` IS NOT IN HERE. It is the "+N more" counter, it is floored above with the rest,
       and it keeps its 44px — it sits on top of the block and is the only way from a week to the
       day that holds what it counts.

       Pinned by `TapTargetFloorTests.A_class_exempt_from_the_floor_says_so_where_the_floor_is
       _defined`, so deleting this rule or its reason fails a test. */
    a.mw-block {
        min-height: 0;
    }

    /* 1b. THE SAME EXCEPTION, FOR THE SAME REASON, ON THE PER-CLINIC BLOCK — SAAS-449. It is not a
       second exception: it is 1a's, for the block that was left an `<article>` when SAAS-378 linked
       the merged grid. `AppointmentCalendarGrid.Placement()` emits `top:{TopPx};height:{HeightPx}`
       exactly as the merged grid's does — the top IS the start time, the height IS the duration (or
       SAAS-161's 36px floor), both in pixels — so the element floor above would draw every block
       the 36px floor holds 44px tall, over the booking after it.
       Nothing else about the block moves: the anchor carries the very same style attribute the
       article carried, and with the floor taken off it is the same box.

       WHAT IS GIVEN UP IS 1a's COST AND NO MORE: a booking drawn at the 36px floor is a 36px target
       rather than 44. A longer booking clears 44px by its own length. `.cal-more` is NOT in here and
       keeps its 44px.

       Pinned by `TapTargetFloorTests.A_class_exempt_from_the_floor_says_so_where_the_floor_is
       _defined`, and by `CalendarPatientLinkTests` for the box itself. */
    a.cal-block {
        min-height: 0;
    }

    /* 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,

    /* The period picker's two halves (SAAS-328). Its own strip rather than the stepper's, because
       it answers a different question — which KIND of period, not which one — and a reader on a
       phone would otherwise have four controls competing for one line. */
    .period-kind a,
    .cal-more,
    .mw-more,
    .quick-ranges a.quick,
    td > a,
    dd > a,
    a.row-link,
    a.clear,
    a.owners-link,
    a.people-link,
    a.branches-link,
    a.rotate-link,
    a.host-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;
    }

    /* THE PERIOD PICKER'S SELECT (SAAS-339), AND IT IS THE ONE CONTROL ON THIS LIST THAT IS NOT A
       LINK, A BUTTON OR A SUMMARY. The element floor above names those four, so a `<select>` reaches
       none of it — a native combo box renders about 30px on a phone and would be the shortest
       target on a Surface whose every other control clears 44.

       NAMED RATHER THAN ADDED TO THE ELEMENT FLOOR, and that is deliberate. A bare `select` here
       would raise every select in the portal, including the report form's, which is DELIBERATELY
       dense (SAAS-163: the floor is never paid by a mouse, and the report form is the screen that
       argument was made about). This is the one select that sits alone in a strip of 44px
       neighbours, so it is the one that owes the floor. */
    .portal-analytics .period-form select,
    .portal-branch .pick-year .year-picker select,
    .portal-closing-days .pick-year .year-picker select {
        min-height: 44px;
    }

    /* ⚠ THE THREE TARGETS IN THE STEPPER STOP TOUCHING (SAAS-353), and this is the one thing that
       slice changed about the strip that is not visible at a desk.

       Until the period label became a control, the strip was two targets with an INERT span between
       them: no two targets shared an edge, and a thumb that landed in the middle hit nothing at all.
       The label is now the primary control of the three, so all three touch — and a mis-hit here is
       not cosmetic. The arrow beside the label steps the period, and a period step is a live reading
       of every clinic database under the reading (SAAS-265: ~30 ms for a twelve-Branch overview,
       165 ms at the largest Branch met). So the very cost SAAS-339 existed to remove is what a
       fat-finger would spend.

       8 px between the three, and the 1 px dividers go with the gap that replaces them — a divider
       floating in the middle of a gap is a line that separates nothing. The frame stays around all
       three, so the strip still reads as one instrument rather than three loose chips. */
    .portal-analytics .month-step {
        gap: var(--s2);
    }

    .portal-analytics .month-step .cur {
        border-inline: 0;
    }

    /* ⚑ THE FOCUS-ON PICKER'S TWO CONTROLS, AND THIS IS A HOLE SAAS-360 FOUND RATHER THAN MADE.
       `.practice-focus` is a `<select>`, which the element floor cannot reach at all — the same
       argument as the period picker's above. `.practice-focus-go` is a `<button>`, which it CAN
       reach, and the reach was already being lost: that class declares `min-height: 32px` outside
       this block, and one class outranks the bare `button` in here. So both halves of the control
       measured 32px on a phone, on every Surface that draws it, and no test could see it —
       TapTargetFloorTests walks anchors, and neither of these is one.
       ⚠ Named rather than added to the element floor above, for the period select's reason: a bare
       `select` there would raise the deliberately dense report form (SAAS-163).

       ⚠⚠ AND THE ELEMENT IS IN THE SELECTOR BECAUSE OF THE CASCADE, WHICH COST A MEASUREMENT. Written
       as `.practice-focus, .practice-focus-go` this rule LOST: one class against one class is a
       specificity tie, and the 32px declarations sit three hundred lines BELOW this block, so source
       order handed it to them. Forcing this media query on in a browser and measuring the control
       still read 32px. `select.` and `button.` break the tie wherever either rule sits. */
    select.practice-focus,
    button.practice-focus-go {
        min-height: 44px;
    }

    /* A GLYPH IS NOT A WORD, so these four 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.

       The fourth is the information mark's button (SAAS-462): an 18px glyph with a 44px hit area on
       a touch screen, drawn with a negative margin so the target is bigger than the line it sits
       on rather than making the line bigger. */
    .form-dialog .dialog-close,
    .report-list li .st,
    .form-dialog .dual .btns .move,
    .operator-editor .dual .btns .move,
    .info-mark-button {
        min-width: 44px;
    }

    .info-mark-button {
        margin-block: -8px;
    }

    /* 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, `.ledger-filters .field-branches`, WAS STYLED HERE until SAAS-510: a fieldset among the other filters. It is
   the clinic picker now, a dropdown above the filter row, and has no rule in this block.) */

/* 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;
}

/* -------------------------------------------------------------------------
   THE PRACTICE AXIS (SAAS-247, ADR 0025 addendum of 2026-09-06)

   A practising account — an Owner or a manager who also treats patients — reads
   either the whole clinic or its own rows. Two controls say so: a band under the
   masthead whenever it is narrowed, and a segmented switch on each own-rows
   Surface.

   COLOUR CARRIES STATE AND NOTHING ELSE (ADR 0020). The band wears the teal
   wash because it IS a status; the switch's lit half wears it because that half
   is the state. Neither is decoration, and neither is the only thing saying it:
   the band says "My practice" in words and the switch carries aria-pressed.
   ------------------------------------------------------------------------- */

/* Full-bleed, directly under the sticky masthead, above the rail. It qualifies
   the whole page rather than the navigation, so it spans both. */
.practice-band {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2) var(--s4);
    padding: var(--s2) var(--s4);
    background: var(--teal-wash);
    border-bottom: 1px solid var(--line);
    color: var(--ink);
    font-size: var(--t-small);
}

.practice-band-said {
    margin: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s1) var(--s2);
}

.practice-band-said strong {
    color: var(--nav-active-ink);
}

/* The way back is a submit button and has to read as the link it replaces — a
   form is what makes it a write, and a write is what this is. */
.practice-band-back {
    appearance: none;
    background: none;
    border: 0;
    padding: 0;
    min-height: 24px;
    color: var(--action);
    font: inherit;
    text-decoration: underline;
    cursor: pointer;
}

.practice-band-back:hover {
    color: var(--action-hover);
}

/* The switch, in each Surface's own control row — the same control in the same place on every page
   that draws it, which SAAS-360 took from seven to nine by adding the two dashboard pages.

   ⚠ `flex-wrap: wrap` ARRIVED WITH THE SCOPE SENTENCE (SAAS-360) AND IS NOT COSMETIC. The row was
   a label, a select and a button, which fit a phone; the sentence saying what the control changes
   does not, and an `inline-flex` row that cannot wrap pushes it off the side of the screen instead.
   Measured at 375: without this the row is 560 px wide inside a 343 px column. */
.practice-switch {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2);
    margin: 0 0 var(--s4);
}

.practice-switch-label {
    color: var(--muted);
    font-size: var(--t-small);
}

/* WHAT THE CONTROL CHANGES, BESIDE THE CONTROL (SAAS-360). `.practice-focus-partial`'s treatment
   verbatim — muted, small — because the two say the same KIND of thing: a fact about the control
   that the control's own words do not carry. It takes a line of its own on a narrow screen through
   the wrap above rather than through a rule of its own. */
.practice-switch-scope {
    color: var(--muted);
    font-size: var(--t-small);
}

.practice-switch .seg {
    display: inline-flex;
    border: 1px solid var(--line);
    border-radius: 6px;
    overflow: hidden;
}

/* Both halves keep their box whichever is lit, so the control does not move
   under the cursor as it is used. The unlit half is the button; the lit half is
   disabled, because pressing the view you are already in is not an act. */
.practice-switch .seg-half {
    appearance: none;
    background: var(--surface);
    border: 0;
    min-height: 32px;
    padding: 0 var(--s3);
    color: var(--muted);
    font: inherit;
    font-size: var(--t-small);
    white-space: nowrap;
    cursor: pointer;
}

.practice-switch .seg-half + .seg-half {
    border-left: 1px solid var(--line);
}

.practice-switch .seg-half:hover:not(:disabled) {
    color: var(--ink-strong);
}

.practice-switch .seg-half.on {
    background: var(--teal-wash);
    color: var(--nav-active-ink);
    font-weight: 650;
    cursor: default;
}

/* THE FOCUS-ON PICKER (SAAS-249). The two-position switch above becomes a
   select the moment the third view exists: two named views said themselves
   well as a segmented pair, and two plus 9 to 64 practitioners per Branch do
   not. The list is the reason, not the fashion.

   With no script on this page the choice needs a submit, so the button is part
   of the control rather than a nicety — see PracticeSwitch.razor. It wears the
   secondary button's shape because pressing it is a navigation, not a save.

   The lit-state colour the segmented switch carried moves onto the BAND alone.
   A select shows its state in its own text, and a teal select would be the one
   place on this page where colour said something the words already said. */
.practice-focus {
    min-height: 32px;
    max-width: 22rem;
    padding: 0 var(--s2);
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    font-size: var(--t-small);
}

.practice-focus-go {
    appearance: none;
    min-height: 32px;
    padding: 0 var(--s3);
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    font-size: var(--t-small);
    cursor: pointer;
}

.practice-focus-go:hover {
    border-color: var(--action);
    color: var(--action);
}

/* A clinic that could not be asked is said out loud beside the control, on
   BranchDoctorList's rule: a list short by one clinic and a list of everybody
   look identical, and only one of them means a colleague is missing from it. */
.practice-focus-partial {
    color: var(--muted);
    font-size: var(--t-small);
}

/* Your practice: the table of one row per Branch. */
.portal-practice .practice-links th,
.portal-practice .practice-links td {
    text-align: left;
    vertical-align: top;
}

/* ======================================================================
   THE DASHBOARD SURFACE (SAAS-315) — the overview and one clinic's own page.

   Scoped under `.portal-analytics` throughout, deliberately. The generic names this Surface wants
   — `.block`, `.tile`, `.grid` — are exactly the names a design system grows collisions on, and
   `.portal-dashboard` is ALREADY TAKEN by the Branches fleet page, which is a different screen.
   One scope class, so nothing here can reach a page it was not written for.

   Every panel is a `.block`, and that is load-bearing rather than cosmetic: two chart palette slots
   fall under the 3:1 mark floor on light `--page` and clear it on `--surface`, so a chart drawn
   straight onto the page would ship two marks that fail. See the --chart-* block above.
   ====================================================================== */

.portal-analytics {
    min-width: 0;
}

.portal-analytics .analytics-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--s3) var(--s5);
    margin-bottom: var(--s2);
}

.portal-analytics .analytics-head h1 {
    margin: 0;
}

/* HOW MANY CLINICS THIS READING COVERS, beside the page's name (SAAS-329). A count rather than a
   badge: it is part of the heading's sentence, so it takes the body size and the muted ink and
   leaves the h1 to carry the weight. Absent entirely on the whole-group page — there is nothing to
   qualify when the answer is "all of them". */
.portal-analytics .analytics-head h1 .h1-of {
    margin-left: var(--s3);
    font-size: var(--t-body);
    font-weight: 500;
    letter-spacing: normal;
    color: var(--muted);
    white-space: nowrap;
}

.portal-analytics .analytics-head > div:first-child {
    min-width: 0;
    flex: 1 1 280px;
}

.portal-analytics .analytics-head p.copy-synced {
    margin: var(--s1) 0 0;
}

.portal-analytics p.latest-bill {
    margin: 2px 0 0;
}

.portal-analytics .lead {
    margin-bottom: var(--s4);
}

/* THE NARROWED PAGE'S OWN LEAD (SAAS-318). A doctor's dashboard looks exactly like a clinic's, only
   quieter, so the page says whose figures it holds before any of them.

   IT WEARS THE PRACTICE BAND'S WASH BECAUSE IT IS THE SAME STATE said in the page's own voice, and
   ADR 0020 puts colour on state and nothing else. Colour is not the only thing saying it either:
   the words carry the whole sentence, and a reader with no colour at all reads the same fact. The
   band is drawn for an account that can SWITCH view; this is drawn for every narrowed reader, which
   is the one that includes a doctor's fenced account. */
.portal-analytics .lead.own-rows {
    padding: var(--s3) var(--s4);
    background: var(--teal-wash);
    border-radius: var(--r-md);
    margin-bottom: var(--s4);
}

.portal-analytics .lead.own-rows strong {
    color: var(--nav-active-ink);
}

.portal-analytics .lead.own-rows .mono {
    font-family: var(--mono);
    font-size: var(--t-small);
}

/* THE EXCEL EXPORT (SAAS-320). Billing's own strip, reused rather than restyled: the two Surfaces
   offer the same thing and a reader crossing between them should not meet a second look. Set at the
   weight of a link and not as a button, because this page's one job is drawing the panels and the
   export is what a reader does once they have the reading they wanted. No new colour. */
.portal-analytics p.export {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s2);
    margin: 0 0 var(--s5);
}

.portal-analytics .export-link {
    font-size: var(--t-body);
    font-weight: 600;
    color: var(--ink);
}

/* THE MONTH STEPPER IS TWO LINKS AND THE LABEL THAT OPENS THE PICKER (SAAS-68, SAAS-353). The whole
   state of this Surface is a period and the period lives in the address bar, which is what keeps the
   page statically rendered and a reading position sendable. The two arrows are links for that
   reason; the label between them is a <summary>, which is clickable and keyboard-operable with no
   script, so the third affordance costs no render mode either.

   ⚠ `overflow: hidden` WENT, AND THE ARROWS' CORNERS CAME BACK BY NAME. What the clip bought was
   rounded corners on the two end arrows' hover wash, and what it cost — once a panel hangs off this
   strip — is a focus ring and a shadow cut off at the strip's own edge. So the two end arrows carry
   their own radii instead: two declarations for the one thing the clip did implicitly. */
.portal-analytics .month-step {
    display: inline-flex;
    align-items: stretch;
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
}

.portal-analytics .month-step .step {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 36px;
    min-height: 36px;
    color: var(--ink);
    text-decoration: none;
    font-size: var(--t-lead);
}

/* The corners the removed `overflow: hidden` used to cut. One inside the other, so the wash stops
   short of the border rather than painting over it. */
.portal-analytics .month-step .step:first-child {
    border-start-start-radius: calc(var(--r-sm) - 1px);
    border-end-start-radius: calc(var(--r-sm) - 1px);
}

.portal-analytics .month-step .step:last-child {
    border-start-end-radius: calc(var(--r-sm) - 1px);
    border-end-end-radius: calc(var(--r-sm) - 1px);
}

.portal-analytics .month-step .step:hover {
    background: var(--row-hover);
}

/* THE LABEL, WHICH IS ALSO THE TRIGGER (SAAS-353). It keeps the ink, the weight and the padding it
   had as a <span>, because a reader who never opens it must still meet the heading they always met
   — "the label still reads as a label" is half of what Darius asked for. What is new is a cursor
   and a caret.

   ⚠ NO `--action` INK AND NO UNDERLINE, which is where this deliberately parts from
   `details.pick-clinics`' summary one strip over (SAAS-510 replaced it with the clinic picker's line). That one was a line of
   chrome offering a control;
   this one is the period the page is reading. Recolouring it would have turned the answer to "which
   month am I looking at" into a link, which is a worse trade than the caret costs. */
.portal-analytics .month-step .cur {
    display: inline-flex;
    align-items: center;
    padding: 0 var(--s3);
    font-weight: 600;
    white-space: nowrap;
    border-inline: 1px solid var(--border);
}

/* THE PERIOD PICKER (SAAS-328 ruling 1). A calendar month, as it always was, or a Year — two links
   in a strip built exactly like the stepper beside it, so the pair reads as one instrument and a
   reader learns nothing new. Both halves are links, which is what keeps the Surface statically
   rendered: the whole state is a period and it lives in the address. */
/* ⚑ AND IT IS THE POSITIONING PARENT OF THE PERIOD PANEL (SAAS-353), WHICH TOOK THREE MEASUREMENTS
   TO GET RIGHT. The panel hangs off the period label inside `.month-step`, so the obvious parents
   were the label and the strip, and BOTH overflow the viewport:

     · anchored to the LABEL, leading edge — at 375 px the label starts one 44 px arrow in from the
       gutter, so a 343 px panel ended 28 px off the right of the screen;
     · anchored to the STRIP, leading edge — this control group is the head's RIGHT column
       (`.analytics-head` is `justify-content: space-between`), so at 1440 the strip's leading edge
       is at x=1232 and a 372 px panel ended at 1604. Measured: `document.scrollWidth` went 1440 to
       1605, which is a horizontal scrollbar on the dashboard.

   THIS GROUP, TRAILING EDGE, `max-width: 100%` is the one that cannot overflow in either direction,
   and it is provable rather than measured: the panel's trailing edge is this group's trailing edge,
   and its width is at most this group's width, so it lies inside a box the page has already fitted.
   `.menu-pop` anchors to a trailing edge for the same reason one band up.

   The price is that at 1440 the panel is as wide as the control group rather than as wide as its own
   contents, so the *Read this* button wraps under the select — which is what it already did at
   375 px, so the two widths now agree rather than differing. */
.portal-analytics .period-pick {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2);
}

.portal-analytics .period-kind {
    display: inline-flex;
    align-items: stretch;
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    overflow: hidden;
}

.portal-analytics .period-kind .kind {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 36px;
    padding: 0 var(--s3);
    color: var(--ink);
    text-decoration: none;
    font-size: var(--t-small);
    font-weight: 600;
    white-space: nowrap;
}

.portal-analytics .period-kind .kind + .kind {
    border-inline-start: 1px solid var(--border);
}

.portal-analytics .period-kind .kind:hover {
    background: var(--row-hover);
}

/* COLOUR CARRIES STATE AND NOTHING ELSE (ADR 0020): the lit half IS the state, exactly as the
   practice switch's is, and it is not the only thing saying so — the link also carries
   aria-current. */
.portal-analytics .period-kind .kind-current {
    background: var(--teal-wash);
    color: var(--ink-strong);
}

/* THE PERIOD PICKER'S JUMP (SAAS-339), NOW ON THE LABEL ITSELF (SAAS-353). Darius: "if we want to
   jump from januari to november, we have to jump through feb march etc, and it will cost query" —
   and then, on the shape that answered it: "we dont need a new combobox for this? just make the
   current month clickable and then it pops up with the period that can be choosen".

   ⚑ WHAT WENT, AND WHY THE RULE IT NEEDED WENT WITH IT. SAAS-339 gave this disclosure
   `flex: 1 0 100%` so it took a LINE OF ITS OWN under the strip: the strip aligns its items on
   their centres, so a form growing inside one of them would have pushed the Month/Year switch and
   the stepper down the page every time somebody opened the picker. That whole problem is gone,
   because the panel no longer grows in flow at all — it is `position: absolute` and floats over the
   page. So the disclosure is a plain flex item of `.month-step`, between the two arrows, and
   nothing below it moves when it opens.

   IT IS DELIBERATELY *NOT* `details.pick-clinics` VERBATIM ANY MORE. That was the right call while
   this was a second line of chrome beside the stepper — two disclosures on one page should not meet
   a reader as two looks. It is the wrong call now: the summary is the PERIOD THE PAGE IS READING,
   and giving it `--action` ink and an underline on hover would turn the answer to "which month am I
   looking at" into a link. What it takes from that disclosure instead is the CARET, which is the
   shell's own `.nav-group` treatment — one glyph that turns rather than two glyphs swapped. */
.portal-analytics details.pick-period {
    display: flex;
    align-items: stretch;
    margin: 0;
}

/* =======================================================================
   ⚑ THE CONTROL BELONGS ON THE LABEL IT CHANGES — THREE SCREENS, ONE GESTURE
   (SAAS-353, SAAS-357)

   Darius, on the dashboard: "we dont need a new combobox for this? just make the current month
   clickable and then it pops up with the period that can be choosen". On the Branch page: "year
   picker in clinic targets should be incoporated in the labels 'target for xxxx' its too far in the
   right top corner". The same correction twice, so it is a product-wide habit rather than two notes:
   THE CONTROL BELONGS ON THE LABEL IT CHANGES, not in a corner of the page.

   THE RULES BELOW ARE WRITTEN ONCE AND NAME ALL THREE PLACES, and the grouping is the point rather
   than economy. Three near-misses of one gesture would be worse than three controls in three
   corners — a reader who learns that the month opens would have to learn again that the year does.
   `DashboardPeriodPickerTests` asserts that each of these selector lists still names all three, so
   a fourth screen is added HERE and a divergence fails a test.

     · the dashboard's period label  — `.portal-analytics details.pick-period`
     · the Branch's Targets heading  — `.portal-branch details.pick-year`
     · Closing days' own heading     — `.portal-closing-days details.pick-year`

   WHAT IS *NOT* SHARED, AND WHY. Each trigger keeps the type it already had — the period label is
   the stepper's `.cur`, the two year labels are `<h2>`s — because "the label still reads as a label"
   is the whole of what Darius asked for, and a shared font size would have flattened a heading into
   a strip item. And each panel keeps its own ANCHOR: the dashboard's control group is the head's
   right column, so its panel hangs from a trailing edge, while both headings start at the content's
   leading edge and theirs hang from that. Anchoring is where geometry differs; everything above is
   where the gesture is the same.
   ======================================================================= */

/* The trigger's own additions. Ink, weight and box stay with each label's own rule — the period
   label's is `.month-step .cur`, the headings' is the `h2` inside them — because a reader who never
   opens one of these must meet the label they always met. */
.portal-analytics details.pick-period > summary,
.portal-branch details.pick-year > summary,
.portal-closing-days details.pick-year > summary {
    cursor: pointer;
    gap: var(--s2);

    /* The marker goes in both engines' spellings — the account menu's own note. A summary that is a
       flex box loses the native triangle in every current engine anyway, so this is belt and braces
       against one that does not. */
    list-style: none;
}

.portal-analytics details.pick-period > summary::-webkit-details-marker,
.portal-branch details.pick-year > summary::-webkit-details-marker,
.portal-closing-days details.pick-year > summary::-webkit-details-marker {
    display: none;
}

/* ⚠ THE FACT THAT IT OPENS IS VISIBLE WITHOUT HOVERING, AND THIS IS THE WHOLE OF THAT. A hover
   wash says "clickable" only to a reader with a pointer who has already found it, and a phone has
   no hover at all; a caret says it to everybody, at rest.

   DRAWN BY THE SHEET RATHER THAN BY THE MARKUP, which is not tidiness. The dashboard strip's
   visible wording is read by `DashboardYearTests` as the reader sees it — "‹ August 2026 ›" — and a
   glyph in the markup would land in that string; on the two headings a glyph in the markup would
   land in the HEADING's own accessible name. Nor is it a loss to a screen reader, because every one
   of these summaries carries an `aria-label` that names the whole control. */
.portal-analytics details.pick-period > summary::after,
.portal-branch details.pick-year > summary::after,
.portal-closing-days details.pick-year > summary::after {
    content: "\25BE";
    flex: 0 0 auto;
    font-size: var(--t-small);
    line-height: 1;
    color: var(--muted);
    transition: transform 160ms ease;
}

/* One caret that turns, so the open state is readable while the transition runs. */
.portal-analytics details.pick-period[open] > summary::after,
.portal-branch details.pick-year[open] > summary::after,
.portal-closing-days details.pick-year[open] > summary::after {
    transform: rotate(180deg);
}

/* The hover wash is the dashboard stepper's own, so the three targets in that strip answer a
   pointer the same way — and the two headings answer it the same way as the strip. Second carrier,
   never the only one: see the caret above. */
.portal-analytics details.pick-period > summary:hover,
.portal-analytics details.pick-period[open] > summary,
.portal-branch details.pick-year > summary:hover,
.portal-branch details.pick-year[open] > summary,
.portal-closing-days details.pick-year > summary:hover,
.portal-closing-days details.pick-year[open] > summary {
    background: var(--row-hover);
}

/* THE PANEL POPS UP, AND "POPS UP" IS CSS (SAAS-353). Darius asked for a label that "pops up with
   the period that can be choosen"; `position: absolute` is the whole of it, and it needs nothing
   interactive. Opening the picker moves NONE of the panels below it — which is what the old
   line-of-its-own arrangement bought with a rule, and this buys by leaving the flow. Measured: the
   standfirst under the head stayed at y=46 at 1440 and y=90 at 375, where before it was pushed to
   109 and 199.

   ⚠ ITS CONTAINING BLOCK IS `.period-pick` AND ITS TRAILING EDGE IS PINNED — see that rule for the
   two anchorings that were measured and thrown away, both of which put a horizontal scrollbar on
   the dashboard. `width: max-content` with `max-width: 100%` is what makes it unoverflowable: as
   wide as its contents want, never wider than a box the page has already fitted.

   `--e2` rather than `--e1`, because this is a floating panel now rather than a card in the flow,
   and `--e2` is `.menu-pop`'s own elevation — the other thing in this product that hangs off a
   summary and covers page content. */
/* THE PANEL'S FRAME, SHARED BY ALL THREE (SAAS-353, SAAS-357). Everything here is the same on the
   dashboard and on the two headings; only the ANCHOR differs, and each screen's own rule below sets
   that. */
.portal-analytics .period-form,
.portal-branch .pick-year .year-picker,
.portal-closing-days .pick-year .year-picker {
    position: absolute;
    top: calc(100% + var(--s2));
    z-index: 20;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s3);
    box-sizing: border-box;
    width: max-content;
    margin: 0;
    padding: var(--s3) var(--s4);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    background: var(--surface);
    box-shadow: var(--e2);
}

/* THE DASHBOARD'S ANCHOR — a TRAILING edge, because this control group is the head's right column.
   See `.period-pick` for the two anchorings that were measured and thrown away. */
.portal-analytics .period-form {
    inset-inline-end: 0;
    max-width: 100%;
}

/* ⚠ A WIDTH FLOOR AND NOT A FIXED WIDTH, AND IT DOES NOT GROW. The longest label the period control
   ever prints is "September 2026 · to date", and a select narrower than its own selected option
   truncates it — so the reader would be told which period they are reading by a string ending in an
   ellipsis. `min-width` is that floor. `flex: 0 1 auto` is the other half and was measured: with
   `1 1 auto` the card is as wide as the head's right column, which at 1440 is seven hundred pixels
   of empty select with the button pushed to the far edge. `max-width: 100%` lets it shrink inside
   the card on a phone rather than forcing a horizontal scroll.

   The two year selects are four digits wide and need none of that argument, and they take the rule
   anyway: three panels that looked different inside would undo what the shared frame above buys. */
.portal-analytics .period-form select,
.portal-branch .pick-year .year-picker select,
.portal-closing-days .pick-year .year-picker select {
    flex: 0 1 auto;
    min-width: 12rem;
    max-width: 100%;
    padding: var(--s2) var(--s3);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
}

/* ⚠ THE OPEN LIST IS PAINTED BY THE PLATFORM, NOT BY THE PAGE, and this rule is a scar the report
   form and the ledger filters already carry. A select whose options inherit the page's colours
   hands a light-on-dark ink to a list the OS draws on its own white, so a dark reader who opens
   this picker meets white text on white — every option but the highlighted one unreadable. `Field`
   and `FieldText` are the system field colours and follow `color-scheme` in both themes. The
   OPTGROUP labels need it too: the period picker is the first select in the portal to carry any.

   ⚑ AND THE TWO YEAR SELECTS NEED IT *BECAUSE* OF THE RULE ABOVE, which is worth knowing before
   somebody trims this list. Until SAAS-357 they were unstyled, so the platform painted them and
   there was nothing to scar; giving them the panel's `background`/`color` is what puts a
   light-on-dark ink into a list the OS draws on white. The two rules travel together or not at
   all. */
.portal-analytics .period-form select option,
.portal-analytics .period-form select optgroup,
.portal-branch .pick-year .year-picker select option,
.portal-closing-days .pick-year .year-picker select option {
    background: Field;
    color: FieldText;
}

/* Outlined, not filled — `.branch-form button`'s own rule and its reason: reading another period is
   how a reader reaches the thing the page is for, not the page's one primary action.

   ⚠ AND THIS IS WHERE THE TWO `Show` BUTTONS FINALLY GET A RULE, WITHOUT SPRINGING SAAS-370'S TRAP.
   That ticket wrote down that the Branch page's `Show` was "still a native button, which is a defect
   of its own", and that Closing days must NOT gain a bare `.portal-closing-days .action` rule —
   boxing `Rename…` beside the destructive `Delete…` would undo half of SAAS-355. Reaching them
   through `.pick-year` names the panel rather than the modifier, so `Rename…` is not in the
   selector's reach at all. The element is named too, not the class: nothing here says `.action`. */
.portal-analytics .period-form button,
.portal-branch .pick-year .year-picker button,
.portal-closing-days .pick-year .year-picker 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;
}

.portal-analytics .period-form button:hover,
.portal-branch .pick-year .year-picker button:hover,
.portal-closing-days .pick-year .year-picker button:hover {
    background: var(--row-hover);
}

/* The clinic chips. A hue is never spent here — this is a strip of eleven codes, and SAAS-255
   ruled that past six Branches a hue stops meaning anything. The code is the identity.

   ONE WRAPPING ROW THAT READS AS TWO (SAAS-337). It carries All, the multi-select disclosure, the
   .per-branch label (which a reader sees as "Per Clinic") and every clinic chip, in that order, and the LABEL is what breaks the line: it
   takes `flex: 1 0 100%`, so it fills a line of its own and the chips start on the next one. That
   keeps one chip loop and one All chip in the markup, and keeps the reading order a screen reader
   follows identical to the order a reader sees — which `order:` would have quietly broken.

   Items are aligned on their BASELINES rather than their boxes, so All's text and the disclosure's
   text sit on one line however tall the open picker makes the disclosure. */
.portal-analytics .branch-chips {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s2);
    margin: 0 0 var(--s5);
}

/* THE LABEL OVER THE CLINIC CHIPS (SAAS-337), which reads "Per Clinic". Quiet and small: it names the row beneath it, and a
   label competing with the codes it heads would be the second thing on the strip asking to be read
   first. `flex-basis: 100%` is the line break — see above. */
.portal-analytics .branch-chips .per-branch {
    flex: 1 0 100%;
    margin: var(--s3) 0 0;
    font-size: var(--t-small);
    font-weight: 600;
    color: var(--muted);
}

.portal-analytics .chip,
.portal-analytics .chip-quiet,
.portal-analytics .chip-current,
.portal-analytics .chip-chosen {
    display: inline-flex;
    align-items: center;
    font-size: var(--t-small);
    font-weight: 600;
    letter-spacing: .03em;
    border: 1px solid var(--border-strong);
    border-radius: 999px;
    padding: 4px var(--s3);
    background: var(--surface);
    color: var(--ink);
    text-decoration: none;
}

.portal-analytics .chip:hover {
    border-color: var(--ink);
}

/* A clinic whose copy could not be read for this window. Achromatic, because it is not a status —
   it is a clinic the reader can still open, and drawing it in amber would make our own sync lag
   look like a fault of theirs. */
.portal-analytics .chip-quiet {
    color: var(--muted);
    font-weight: 500;
}

.portal-analytics .chip-current {
    background: var(--teal-wash);
    border-color: transparent;
    color: var(--nav-active-ink);
}

/* A CLINIC IN THE BRANCH SELECTION (SAAS-329). The chip row carries every clinic the reader may
   see, so the chosen ones have to be legible as a set at a glance — and it must not be the same
   mark as .chip-current, because All is a place the reader can go and a chosen clinic is a clinic
   in the arithmetic.

   ⚠ THE TICK IS DRAWN AND NOT SPELLED. The chip's own text is the short code, which the reader
   matches against the table below; prefixing a glyph into the label would change the string they
   are matching. So the mark is the chip's fill and a tick in the ::before, which the accessible
   name never carries — `aria-current` and the title say it in words for anybody not reading fills. */
.portal-analytics .chip-chosen {
    background: var(--teal-wash);
    border-color: var(--nav-active-ink);
    color: var(--nav-active-ink);
    font-weight: 600;
}

.portal-analytics .chip-chosen::before {
    content: "\2713";
    margin-right: 4px;
    font-weight: 700;
}

/* (THE PICKER'S DISCLOSURE, `details.pick-clinics`, WAS HERE UNTIL SAAS-510: "Select Several Clinics", a flex item of
   .branch-chips beside All whose form grew downward inside the row. The multi-select is the clinic picker now, a dropdown
   line above the filter row whose panel floats over the page (.clinic-picker-*, above), so nothing here is left to style.) */

/* The tiles. A number in a box, and the right answer for a tile is NOT a chart (SAAS-266). */
.portal-analytics .tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
    gap: var(--s3);
    margin: 0 0 var(--s5);
}

.portal-analytics .tiles-in-block {
    margin: 0;
}

.portal-analytics .tile {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    box-shadow: var(--e1);
    padding: var(--s3) var(--s4);
    min-width: 0;
}

.portal-analytics .tile .k {
    color: var(--muted);
    font-size: var(--t-micro);
    text-transform: uppercase;
    letter-spacing: .04em;
}

.portal-analytics .tile .v {
    font-size: var(--t-h2);
    font-weight: 600;
    letter-spacing: -.02em;
    line-height: 1.25;
    margin-top: 2px;
    font-variant-numeric: tabular-nums;
}

.portal-analytics .tile .n {
    color: var(--muted);
    font-size: var(--t-micro);
    margin-top: 2px;
}

/* The panels. `break-inside: avoid` is for the print path the Day End already established: a chart
   cut across two pages is a chart nobody can read. */
.portal-analytics .block {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    box-shadow: var(--e1);
    padding: var(--s4) var(--s5);
    min-width: 0;
    break-inside: avoid;
}

.portal-analytics .block > h2 {
    margin: 0 0 var(--s1);
    font-size: var(--t-h3);
}

.portal-analytics .block > h3 {
    font-size: var(--t-small);
    font-weight: 600;
    color: var(--muted);
    margin: 0 0 var(--s1);
}

.portal-analytics .block .sub {
    color: var(--muted);
    font-size: var(--t-small);
    margin: 0 0 var(--s3);
}

/* A MARK BESIDE A CHART'S HEADING HANGS ITS PANEL FROM THE BLOCK, NOT FROM THE MARK (SAAS-485).
   The explanations that used to sit under these headings are behind a mark now, and the mark sits at the
   end of a heading in whichever column the block is in. A panel hung from its mark's left edge ran 31 px
   past a 1440 px screen from the third block of a row (Patients by doctor), and a block's heading can end
   anywhere along it. So the block is the positioning box and the panel starts at its content edge, which
   is what the phone's rule does with the page's gutters: the mark stops being the box, and the panel's
   vertical place stays directly under it (`top: auto`). */
.portal-analytics .block {
    position: relative;
}

.portal-analytics .block > h2 + .info-mark {
    position: static;
}

.portal-analytics .block > h2 + .info-mark > .info-mark-panel {
    top: auto;
    left: var(--s5);
    max-width: min(40ch, calc(100% - var(--s5) - var(--s4)));
}

.portal-analytics .analytics-grid {
    display: grid;
    gap: var(--s4);
    margin: 0 0 var(--s4);
    align-items: start;
}

.portal-analytics .analytics-stack {
    display: grid;
    gap: var(--s4);
    align-content: start;
    min-width: 0;
}

.portal-analytics .analytics-grid.cols-2 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
}

.portal-analytics .analytics-grid.cols-3 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
}

@media (max-width: 1180px) {
    .portal-analytics .analytics-grid.cols-3 {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

@media (max-width: 860px) {
    .portal-analytics .analytics-grid.cols-2,
    .portal-analytics .analytics-grid.cols-3 {
        grid-template-columns: 1fr;
    }
}

/* Charts. `max-width: 100%` and `height: auto` are what make an SVG with a viewBox behave in a
   grid cell at 375px without a single media query of its own. */
.portal-analytics figure {
    margin: 0;
    min-width: 0;
}

.portal-analytics figcaption {
    color: var(--muted);
    font-size: var(--t-micro);
    margin-top: var(--s2);
}

.portal-analytics .chart svg {
    display: block;
    max-width: 100%;
    height: auto;
}

.portal-analytics .chart svg text {
    font-family: inherit;
}

/* THE TWO TWELVE-MONTH CHARTS CANNOT SHRINK TO A PHONE (SAAS-328). Every other chart on this
   Surface fits four columns or six bands into a 330-unit viewBox and scales down cleanly; the
   Multi-year comparison carries twelve month labels and up to five series across 660 units, and a
   Year's by-month panel grows to 874 for its twelve columns — so at 343px `max-width: 100%` would
   render their 10px type at four or five. Both figures scroll inside themselves below the
   two-column break: the answer .tblbox already gives a wide table, and the Figures table underneath
   each carries every number for a reader who would rather not scroll. */
@media (max-width: 860px) {
    .portal-analytics .chart-series,
    .portal-analytics .chart-columns {
        overflow-x: auto;
    }

    .portal-analytics .chart-series svg,
    .portal-analytics .chart-columns svg {
        max-width: none;
    }
}

/* The gauge is centred in its panel rather than stretched across it. The cap is the SVG's own
   declared width now (see ChartGauge): a viewBox alone gives an element a ratio and no intrinsic
   width, which is what let this chart fill a 520px column and overflow a 375px one. */
.portal-analytics .chart-gauge svg.chart-gauge-svg {
    margin: 0 auto;
}

.portal-analytics .chart-notarget {
    text-align: center;
    padding: var(--s5) 0;
}

.portal-analytics .chart-hero {
    font-size: var(--t-h1);
    font-weight: 650;
    letter-spacing: -.02em;
    color: var(--ink-strong);
    margin: 0;
}

.portal-analytics .chart-hero-unit {
    font-size: var(--t-small);
    font-weight: 500;
    color: var(--muted);
    letter-spacing: 0;
    margin-inline-start: var(--s2);
}

.portal-analytics .chart-gauge figcaption {
    text-align: center;
}

.portal-analytics .chart-stated {
    margin-top: var(--s2);
}

/* THE CATEGORY DONUT'S PANEL, AND IT IS TWO LAYOUTS ON PURPOSE (SAAS-327).
   ⚠ WIDE IS THE DEFAULT AND NARROW IS THE OVERRIDE, because the wide layout is the one the ticket
   ruled and the narrow one is what this panel already shipped as.

   Wide: `[ring with its labels around it]` above `[the other categories]` — one column, the ring
   centred in it, the unlabelled categories listed underneath. The ring gains what the legend grid
   gave up: 140 units across against 124, in a panel of the same width.

   Narrow (at or below the two-column break): `[ring | legend grid beside it]`, exactly as before.
   The ring there is 150 units and carries no leaders — DonutLeaders.Frames says why in full.

   ⚠ THE RING IS CENTRED AT ITS OWN DECLARED WIDTH RATHER THAN STRETCHED TO THE PANEL, and that is
   the gauge's own answer above. The two panels it appears in are not the same width — 405px on a
   clinic's page and 640px on the group's, measured at 1440 — so a ring that grew with its panel
   would draw the same chart at two sizes and a reader comparing a clinic with the group would be
   comparing two rings. The whitespace on the wider panel is the price of that.

   ⚠ AND PRINT FALLS ON THE NARROW SIDE, because a sheet of paper is under 860px: a printed panel
   keeps the legend it prints today. That is where the existing break puts it rather than a ruling
   of its own, and which layout SHOULD print is SAAS-319's question. */
.portal-analytics .donut-panel {
    display: block;
}

.portal-analytics .donut-panel > .chart-key {
    display: none;
}

.portal-analytics .chart-donut svg.donut-narrow {
    display: none;
}

.portal-analytics .chart-donut svg.donut-wide {
    margin: 0 auto;
}

.portal-analytics .donut-rest {
    margin-top: var(--s3);
}

/* The h3 the block's own rule cannot reach: this heading sits inside the donut component rather
   than as a direct child of the panel, so it restates that one declaration set and nothing else. */
.portal-analytics .donut-rest > h3 {
    font-size: var(--t-small);
    font-weight: 600;
    color: var(--muted);
    margin: 0 0 var(--s1);
}

@media (max-width: 860px) {
    .portal-analytics .donut-panel {
        display: flex;
        flex-wrap: wrap;
        gap: var(--s4);
        align-items: center;
    }

    .portal-analytics .donut-panel > .chart {
        flex: 0 0 auto;
    }

    .portal-analytics .donut-panel > .chart-key {
        display: flex;
        flex: 1 1 10rem;
    }

    .portal-analytics .chart-donut svg.donut-wide {
        display: none;
    }

    .portal-analytics .chart-donut svg.donut-narrow {
        display: block;
    }

    .portal-analytics .donut-rest {
        display: none;
    }
}

/* THE ONE INTERACTION THE EXAMPLE HAS THAT CSS GIVES FOR FREE. Its by-doctor charts scroll past
   about twelve names, and `overflow-y: auto` is the whole of it — no library, no script
   (SAAS-266). Senayan carries 21 performers over a four-month span. */
.portal-analytics .chart-scroll {
    max-height: 20.5rem;
    overflow-y: auto;
    overflow-x: hidden;
}

.portal-analytics .stacked + .stacked {
    margin-top: var(--s3);
}

/* The key. Every chart carries one, because a mark has to be identifiable without colour — a
   legend beside direct value labels and a figures table underneath is the three-way redundancy the
   dataviz method asks for. */
.portal-analytics .chart-key {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2) var(--s4);
    margin: var(--s3) 0 0;
    padding: 0;
    list-style: none;
    font-size: var(--t-small);
}

.portal-analytics .chart-key li {
    display: flex;
    align-items: center;
    gap: var(--s2);
    color: var(--ink);
    min-width: 0;
}

.portal-analytics .chart-key .sw {
    width: 12px;
    height: 12px;
    border-radius: 3px;
    flex: 0 0 auto;
}

/* ⚑ .sw-line IS BACK, RULED BY DARIUS ON 2026-09-20 (SAAS-356): the year in hand is solid and every
   prior year is dashed, and a legend that drew both as identical blocks would separate the years by
   HUE ALONE — the one thing that ruling closes. So a year's key draws the LINE it stands for.

   It went away on 2026-09-18 and came back for a different reason than it left with. SAAS-336 drew
   a line key because every prior year wore the same grey; the hue-per-year ruling made the colour
   carry the year again and the shape had nothing left to say. Now the SHAPE carries the solid/dashed
   distinction, which the colour cannot.

   30px wide, because that is what a dash pattern needs to repeat: at PriorYearDash the swatch draws
   dash-gap-dash rather than one ambiguous stub. And it is an inline SVG rather than a background, so
   it prints whatever a printer does with backgrounds — see (8) in the print block. */
.portal-analytics .chart-key .sw-line {
    width: 30px;
    height: 12px;
    flex: 0 0 auto;
    overflow: visible;
}

/* The ink swatch wears the same 45 degree hatch the ink MARK does. It has to be said twice — once
   as an SVG <pattern> for the mark and once as a gradient for the swatch — and the two are defined
   beside each other for exactly that reason. */
.portal-analytics .chart-key .sw-ink {
    background: repeating-linear-gradient(45deg,
        var(--chart-ink) 0 2px,
        color-mix(in srgb, var(--chart-ink) 35%, transparent) 2px 5px);
}

.portal-analytics .chart-key .v {
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

/* The figures under every chart. A <details> rather than a scripted panel, for the Day End block's
   reason: it survives with no circuit, it is keyboard-operable and announced with no aria of ours,
   and print.js can force it open by setting one attribute. */
.portal-analytics details.figures {
    margin-top: var(--s3);
}

.portal-analytics details.figures > summary {
    cursor: pointer;
    color: var(--muted);
    font-size: var(--t-small);
}

.portal-analytics details.figures[open] > summary {
    margin-bottom: var(--s2);
}

.portal-analytics .tblbox {
    overflow-x: auto;
}

.portal-analytics table.analytics-rows {
    border-collapse: collapse;
    width: 100%;
    font-size: var(--t-body);
}

.portal-analytics table.analytics-rows.dense {
    font-size: var(--t-small);
}

.portal-analytics table.analytics-rows th,
.portal-analytics table.analytics-rows td {
    padding: var(--row-y) var(--s3);
    border-bottom: 1px solid var(--border);
    text-align: left;
    vertical-align: top;
}

.portal-analytics table.analytics-rows.dense th,
.portal-analytics table.analytics-rows.dense td {
    padding: 6px var(--s2);
}

.portal-analytics table.analytics-rows thead th {
    color: var(--muted);
    font-weight: 600;
    font-size: var(--t-micro);
    text-transform: uppercase;
    letter-spacing: .04em;
}

.portal-analytics table.analytics-rows tbody th[scope="row"] {
    text-transform: none;
    letter-spacing: 0;
    font-size: inherit;
    color: var(--ink);
    font-weight: 600;
}

.portal-analytics table.analytics-rows td.num,
.portal-analytics table.analytics-rows th.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.portal-analytics table.analytics-rows tbody tr:hover td {
    background: var(--row-hover);
}

.portal-analytics table.analytics-rows tr.totals td,
.portal-analytics table.analytics-rows tr.totals th {
    font-weight: 600;
    border-top: 1px solid var(--border-strong);
    border-bottom: 0;
}

.portal-analytics table.analytics-rows td.stated,
.portal-analytics table.analytics-rows tr.stated-row td {
    color: var(--muted);
    font-size: var(--t-small);
}

/* The Branch name in the dashboard's per-Branch table is the handle into that Branch's own
   dashboard, and it is drawn by `a.row-link` alone since SAAS-363 — the third copy of the rule
   that hid the affordance, found by the audit rather than reported. See A NAME THAT OPENS A
   THING. */

.portal-analytics .hint {
    display: block;
    color: var(--muted);
    font-size: var(--t-micro);
    font-weight: 400;
}

.portal-analytics .status {
    display: inline-flex;
    align-items: center;
    gap: var(--s1);
    font-weight: 600;
    white-space: nowrap;
}

.portal-analytics .status.ok {
    color: var(--ok);
}

.portal-analytics .status.warn {
    color: var(--warn);
}

.portal-analytics .status.bad {
    color: var(--bad);
}

.portal-analytics .analytics-foot {
    border-top: 1px solid var(--border);
    padding-top: var(--s3);
    margin-top: var(--s5);
    max-width: var(--measure);
}

/* The definitions block the SVG <pattern> lives in. It draws nothing and must take no room. */
.portal-analytics .chart-defs {
    position: absolute;
    width: 0;
    height: 0;
}

/* ================================================================================================
   THE PRINTED DASHBOARD (SAAS-319 — ruled on SAAS-271, 2026-09-12)

   The Surface inherits the Day End document's printed posture whole rather than re-deciding it: the
   page prints, the browser's own Save as PDF is the PDF path, there is no server pipeline and no
   print button of ours, and print.js forces the light theme. So this block declares NO COLOUR of
   its own, for the reason the Day End's block declares none — there is one palette in this product
   and printing SELECTS from it. `DashboardPrintTests` holds that, separately from the day end's.

   WHAT THIS BLOCK DECIDES, and every one of them is a defect seen on a real capture:

     1. THE CHROME GOES — the crumb, the period control and the clinic chips. `.month-step` was the
        whole of the period control when this block was first written; SAAS-333 put a Month/Year
        switch beside it inside `.period-pick`, and hiding only the stepper would have printed a
        pair of links reading "Month Year" with no period anywhere on the sheet.
     2. THE MASTHEAD COMES BACK. The facts a printed document must carry — the Tenant, the clinics,
        the period — are on screen in the site masthead and the period control, and BOTH are chrome.
        So `.doc-masthead` is the one thing here that print reveals rather than hides; it is
        `display: none` on screen, twenty lines above.
     3. THE ARRANGEMENT IS UNDONE. A two- or three-column grid has nothing beside it on paper: the
        sheet is narrower than the two-column break, so every grid was going to be one column
        anyway, and a block container fragments where a grid container is the engine's own weak
        spot. The gap goes with the grid, so the panels carry their own margin.
     4. PANELS DO NOT SPLIT UNLESS THEY CANNOT FIT — `break-inside: avoid` on the panel and
        `break-after: avoid` on its heading: a gauge cut by a page edge is a defect, not a layout.
        ⚠ A PANEL TALLER THAN A SHEET IS SPLIT ANYWAY, by the engine, whatever this asks — the
        Multi-year comparison carries three charts and three tables and cannot fit. That is why the
        rule is also stated at the FIGURE and at the ROW: when a break must happen inside a panel it
        then lands between two charts rather than through one. It is the Day End's own reasoning at
        one level down (its block 4: avoid on the row, never on the table).

        ⚑ AND THAT IS A RULING RATHER THAN A DEFECT, MADE BY DARIUS ON 2026-09-20 (SAAS-319). The
        criterion first written was "no panel is cut by a page edge in any capture", and it is
        UNACHIEVABLE — not through any failing of this block, but because the engine paginates a box
        taller than a sheet whatever `break-inside` asks. Commit a2fd7c2 named the live case: PIK's
        `Patients by doctor` splits across print sheets 6-7. So the criterion is RELAXED, in these
        words: PANELS DO NOT SPLIT UNLESS THEY CANNOT FIT. A panel cut by a page edge is not to be
        re-raised as a bug against this block; what IS a bug is a break landing through a chart
        rather than between two, which is what the figure and the row rules are for.

        AND A TABLE THAT CANNOT FIT REPRINTS ITS COLUMN HEADINGS — the same ruling, and the ordinary
        convention for a long table in a printed report: a reader holding sheet 7 has to be able to
        tell what each column means without flipping back. The mechanism is `display:
        table-header-group` on the thead, stated with the row rule below.

        ⚠ It was verified on a table that ACTUALLY continues rather than on a short fixture, since
        a header group that never continues proves nothing. The by-doctor table under PIK's
        `Patients by doctor` prints its `Doctor / Treatments / Patients / Sales` heading on BOTH
        sheets its rows run across; deleting this rule leaves that heading on the first of the two
        and nothing at all on the second.
     5. THE CLIPS OPEN, and this is the only rule here whose absence prints a WRONG document rather
        than an ugly one. Three boxes scroll on screen and a sheet of paper cannot scroll: the wide
        tables (`.tblbox`), the by-doctor panels (`.chart-scroll`) and — since SAAS-333 — the two
        twelve-month charts, which take `overflow-x: auto` and `max-width: none` below the 860px
        break. Paper is under 860px, so a Year's by-month columns (874 units) would have printed
        with its last months simply missing and nothing on the sheet to say so.
     6. THE FIGURES TABLE TWIN PRINTS. Ruling 6: both, not one. `details.figures` is closed by
        default, and a closed <details> hides its content in the browser's own shadow tree — print
        cannot reach it from here, so print.js sets `open` and this only removes the affordance that
        has stopped meaning anything.
     7. ⚠ THE DONUT PRINTS WITH ITS LEADER LINES, WHICH IS SAAS-327'S OPEN QUESTION ANSWERED HERE.
        The panel is two layouts (see the donut's own section above): WIDE is the ring with labels
        on leader lines around it, NARROW is a smaller ring with the full legend grid beside it. The
        switch is the 860px break, and paper is about 717px — A4 less the printer's margins — so
        print INHERITED narrow. Ruled deliberately the other way, for one reason: THE LEGEND ASKS
        THE READER TO RESOLVE A COLOUR AND PAPER IS WHERE THAT RELATIONSHIP IS WEAKEST. A swatch is
        a CSS background, which browsers drop from print by default, and a clinic's printer may well
        be monochrome — five palette colours as five greys is SAAS-336's defect on a different
        ground. A leader points by POSITION, which survives a black-and-white printer and a dropped
        background alike. Nothing is lost by dropping the legend: the ring's five labels, the list
        of the other categories beneath it (`.donut-rest`, which narrow hides and print restores)
        and the Figures table under the panel carry every category between them. All four rules are
        stated rather than three, so print takes the whole of one layout and never half of each.
     8. THE ONE FILL WHOSE MEANING IS THE FILL. A fill's legend swatch is a <span> with a
        background (ChartLegend says why: it must wear the ink hatch), so it is exactly what a
        printer drops — a key of unmarked names. `print-color-adjust: exact` on it, and on nothing
        else here: every chart MARK is SVG geometry, which prints whatever the background setting
        says, a LINE's swatch is an inline SVG for the same reason, and the tile's own `--surface`
        is what paper already is.
   ============================================================================================= */

/* The printed sheet's masthead — see (2) in the block below. On screen the site masthead carries
   the Tenant a hundred pixels above this and the period control carries the period, so drawing it
   here as well would say both twice.

   Its type is declared HERE rather than in the print block, and deliberately: everything below is
   inert while the element is hidden, and it keeps the print block to layout with not one
   declaration about ink in it. Tokens either way — `--muted` is the light theme's own, because
   print.js has stamped `data-theme="light"` by the time this is visible. */
.portal-analytics .doc-masthead {
    display: none;
    margin: 0 0 var(--s3);
    padding-bottom: var(--s2);
    border-bottom: 1px solid var(--border-strong);
    color: var(--muted);
    font-size: var(--t-small);
}

@media print {
    /* (1) — and the export strip joined it with SAAS-320, for the same reason everything else in
       this rule is here: it is a control, and a sheet of paper offering a download is a control a
       reader cannot work. */
    .portal-analytics .crumb,
    .portal-analytics .period-pick,
    .portal-analytics .month-step,
    .portal-analytics .branch-chips,
    .portal-analytics .practice-switch,
    .portal-analytics p.export,
    .portal-analytics .dashboard-filters,
    .portal-analytics .dashboard-export {
        display: none;
    }

    /* (2) */
    .portal-analytics .doc-masthead {
        display: block;
    }

    /* (3) */
    .portal-analytics .analytics-grid {
        display: block;
    }

    .portal-analytics .analytics-grid > .block {
        margin-bottom: var(--s4);
    }

    /* The overview's left column is a stack of blocks (SAAS-599). On paper it is block flow like the
       grid around it, so each block breaks between sheets on its own and keeps the grid's gap. */
    .portal-analytics .analytics-stack {
        display: block;
    }

    .portal-analytics .analytics-stack > .block {
        margin-bottom: var(--s4);
    }

    .portal-analytics .block {
        box-shadow: none;
    }

    /* (4) */
    .portal-analytics .block,
    .portal-analytics .tile,
    .portal-analytics figure.stacked {
        break-inside: avoid;
    }

    .portal-analytics .block > h2,
    .portal-analytics .block > h3,
    .portal-analytics figure.stacked > h3 {
        break-after: avoid;
    }

    /* (5) */
    .portal-analytics .tblbox {
        overflow: visible;
    }

    .portal-analytics .chart-scroll {
        max-height: none;
        overflow: visible;
    }

    .portal-analytics .chart-series,
    .portal-analytics .chart-columns {
        overflow-x: visible;
    }

    .portal-analytics .chart-series svg,
    .portal-analytics .chart-columns svg {
        max-width: 100%;
    }

    /* (4) again, at the grain a long table has to flow at — the heading on every sheet its rows run
       onto, and never a row split down the middle of itself.

       ⚑ RULED 2026-09-20 (SAAS-319), and this is the rule that ruling turns on: a table too tall
       for one sheet MAY break, and REPRINTS its column headings at the top of the continuation
       sheet. See (4) above for the relaxed criterion and for the case it was checked on — a header
       group that never continues proves nothing, so it was verified on a table that really does
       continue. Deleting this one line is invisible on screen and on any sheet that happens to fit. */
    .portal-analytics thead {
        display: table-header-group;
    }

    .portal-analytics tr {
        break-inside: avoid;
    }

    /* (6) */
    .portal-analytics details.figures > summary {
        list-style: none;
        cursor: auto;
    }

    .portal-analytics details.figures > summary::-webkit-details-marker {
        display: none;
    }

    /* (7) */
    .portal-analytics .donut-panel {
        display: block;
    }

    .portal-analytics .donut-panel > .chart-key {
        display: none;
    }

    .portal-analytics .chart-donut svg.donut-wide {
        display: block;
        margin: 0 auto;
    }

    .portal-analytics .chart-donut svg.donut-narrow {
        display: none;
    }

    .portal-analytics .donut-rest {
        display: block;
    }

    /* (8) */
    .portal-analytics .chart-key .sw {
        -webkit-print-color-adjust: exact;
        print-color-adjust: exact;
    }
}

/* =======================================================================
   ONE BRANCH'S OWN PAGE, AND CLOSING DAYS (SAAS-330)

   The Targets screen these came out of carried no rules of its own — it leaned on the shell and on
   browser defaults, which was survivable for six rows of a table and is not for a twelve-cell grid
   somebody types into on a phone. Everything below is tokens, so both themes come for free.
   ======================================================================= */

.portal-branch .crumb,
.portal-closing-days .crumb {
    margin: 0 0 var(--s2);
    color: var(--muted);
    font-size: var(--t-small);
}

.portal-branch .crumb a,
.portal-closing-days .crumb a {
    color: var(--muted);
}

.portal-branch .crumb a:hover,
.portal-closing-days .crumb a:hover {
    color: var(--ink);
}

.portal-branch .page-head,
.portal-closing-days .page-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s4);
    flex-wrap: wrap;
}

/* THE TITLE'S OWN ROW (SAAS-478): the Closing days title is a `.page-head` — a block-level flex row
   that holds the title and nothing else — and its mark is drawn right after it, which put the mark on
   the line under the title, alone. The head is one box on the title's line instead, as wide as the
   title, so the mark is beside it. The row where the year heading and its action sit has no mark after
   it and still spaces out. */
.portal-closing-days .page-head:has(+ .info-mark) {
    display: inline-flex;
    vertical-align: middle;
    max-width: calc(100% - var(--s7));
}

/* ⚑ THE YEAR PICKER IS NOW THE HEADING THAT NAMES THE YEAR (SAAS-357). Darius: "year picker in
   clinic targets should be incoporated in the labels 'target for xxxx' its too far in the right top
   corner". Both pages printed the year in a heading and put the means of changing it in the other
   corner; the heading is the trigger now, and the frame, caret, select and button come from the
   shared block up in the analytics section — see "THE CONTROL BELONGS ON THE LABEL IT CHANGES".

   `position: relative` on the DISCLOSURE, which is what the dashboard could not do: there the
   control group is the head's right column, so a leading-edge panel ran off 1440. Both of these
   headings start at the content column's leading edge, so their panels hang from that and have the
   whole column to grow into.

   ⚠ THE HEADING SITS INSIDE THE SUMMARY, which is the spelling the spec provides — `<summary>`
   takes "phrasing content, optionally intermixed with heading content". The ticket asked for
   `<details>` inside the `<h2>`; that is invalid (an `<h2>` takes phrasing content) and it was
   MEASURED to give the heading an accessible name of "Targets for 2026 Year 20262025 Show", open or
   shut. This way round the heading's own name survives intact in both states.

   So the `<h2>` loses its margins to the summary, which now carries the spacing the heading had. */
.portal-branch .pick-year,
.portal-closing-days .pick-year {
    position: relative;
    margin: 0;
}

/* ⚠ THE SUMMARY INHERITS THE HEADING'S SPACING, BECAUSE THE HEADING GAVE IT UP. An `<h2>` carries
   0.83em of margin above and below from the browser itself — 17.4 px at this size — and that margin
   is what separated the heading from the prose either side of it. Zeroing it inside the summary
   (the rule below) without putting it back here measured as a `.page-head` 41 px tall where it had
   been 96, with the heading jammed against the paragraph above. `var(--s4)` is the token nearest
   the margin it replaces.

   The negative inline margin is optical alignment and not a nudge: the summary's own padding is
   what the hover and open wash needs, and without pulling it back by the same amount the heading's
   TEXT would sit 8 px in from every paragraph on the page. */
.portal-branch .pick-year > summary,
.portal-closing-days .pick-year > summary {
    display: inline-flex;
    align-items: baseline;
    margin-block: var(--s4);
    margin-inline-start: calc(var(--s2) * -1);
    padding: var(--s1) var(--s2);
    border-radius: var(--r-sm);
}

.portal-branch .pick-year > summary > h2,
.portal-closing-days .pick-year > summary > h2 {
    margin: 0;
}

/* ⚠ A LEADING-EDGE ANCHOR NEEDS ITS OWN CEILING, because `max-width: 100%` would be 100% of a
   heading and a heading can be narrower than the select inside the panel. The viewport term is what
   keeps the panel on a 375 px screen: the heading starts at the content gutter, so a panel of at
   most `100vw` less two gutters cannot reach the far edge. 22rem is a hair wider than the panel's
   own contents want, so on a desktop it never bites. */
.portal-branch .pick-year .year-picker,
.portal-closing-days .pick-year .year-picker {
    inset-inline-start: 0;
    max-width: min(22rem, calc(100vw - 2 * var(--s4)));
}

/* THE PRACTITIONER TABLE'S MONTH PICKER (SAAS-318) — the year picker's own control, reused rather
   than restyled: the two pick a period on the same page and a reader should not meet two looks.
   It is a separate control because the two tables have different grains — the branch's is twelve
   cells for one year and a practitioner's is one cell per person for one month. */
.portal-branch .month-picker {
    display: flex;
    align-items: center;
    gap: var(--s2);
    margin: 0 0 var(--s3);
}

.portal-branch .month-picker label {
    color: var(--muted);
    font-size: var(--t-small);
}

/* ONE FORM PER ROW, laid out inside the cell it belongs to. The figure and its Save sit on one
   line, and they wrap together on a phone rather than the button dropping under the next row's
   name. */
.portal-branch .cell-form {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--s2);
    flex-wrap: wrap;
    margin: 0;
}

.portal-branch .cell-form input {
    max-width: 12rem;
    text-align: right;
}

.portal-branch section,
.portal-closing-days section.target-trail {
    margin-top: var(--s5);
}

.portal-branch .lead,
.portal-branch .hint {
    max-width: var(--measure);
}

/* --- The twelve cells -----------------------------------------------------

   A grid rather than a table, because each cell is a LABELLED FIELD and not a value in a row: a
   table would need a header row naming the months and a body row holding the boxes, which reads as
   one twelve-column scroll on a phone and as a header nobody needs on a desktop.

   ⚠ SIX ACROSS, FIXED, AND NOT `auto-fit`. Auto-fit gave eleven columns on a wide page and left
   December alone on a second row, which reads as a mistake rather than as a year. Six and six is
   the half-year split every clinic already thinks in, and it holds at every width down to the
   tablet break. The form is capped at a measure of its own so a 1440 px page does not stretch each
   box to 8 rem for a nine-digit figure. */
.portal-branch .target-year-form {
    max-width: 62rem;
}

.portal-branch .target-grid {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: var(--s3);
    margin: var(--s4) 0;
}

/* Three across on a tablet, two on a phone — the month name stays above its own box either way, so
   nothing has to be read across a scroll. */
@media (max-width: 900px) {
    .portal-branch .target-grid {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

@media (max-width: 480px) {
    .portal-branch .target-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

.portal-branch .target-grid .cell {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.portal-branch .target-grid .cell span {
    color: var(--muted);
    font-size: var(--t-small);
}

.portal-branch .target-grid input {
    /* The padding and border inside the cell's width (SAAS-473 part b): at 100% plus both, each box
       ran 2 px past a 375 px phone and over the gap beside it at every width. */
    box-sizing: border-box;
    width: 100%;
    padding: var(--s2);
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    /* Tabular figures, so a column of targets lines up on its digits rather than on its glyph
       widths — the one thing a reader does with this grid is compare twelve numbers. */
    font-variant-numeric: tabular-nums;
    text-align: right;
}

.portal-branch .target-grid input:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 1px;
    border-color: var(--action);
}

/* A locked cell is still a cell — same box, same figures, no caret. Greying it to the point of
   illegibility would take away the reading that ADR 0033 ruling 5 deliberately leaves in. */
.portal-branch .target-grid input[readonly] {
    background: var(--code-surface);
    color: var(--muted);
    border-style: dashed;
}

.portal-branch .form-foot {
    display: flex;
    justify-content: flex-end;
    margin-top: var(--s3);
}

.portal-branch .weekday-form {
    max-width: 62rem;
}

.portal-branch .weekdays {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2) var(--s4);
    border: 0;
    padding: 0;
    margin: var(--s3) 0;
}

.portal-branch .weekdays label {
    display: flex;
    align-items: center;
    gap: 6px;
    /* 44 px of tappable height, SAAS-324's floor — seven boxes in a row is exactly the control a
       thumb misses. */
    min-height: 44px;
}

.portal-branch .weekdays[disabled] label {
    color: var(--muted);
}

.portal-branch table.target-reading {
    border-collapse: collapse;
    margin-top: var(--s4);
}

.portal-branch table.target-reading th,
.portal-branch table.target-reading td {
    border-bottom: 1px solid var(--border);
    padding: var(--s2) var(--s4) var(--s2) 0;
    text-align: left;
}

.portal-branch table.target-reading th {
    color: var(--muted);
    font-weight: 500;
}

.portal-branch table.target-reading td.money {
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}

/* --- The two listings ---------------------------------------------------- */

/* Capped at a measure rather than run to the window's edge: the row actions are the rightmost
   column, and on a 1440 px page a full-width table puts "Remove…" nine hundred pixels from the date
   it removes. `.site-main` sets no width of its own, so the cap belongs here. */
.portal-branch table.trail-rows,
.portal-closing-days table.trail-rows,
.portal-closing-days table.closure-rows {
    width: 100%;
    max-width: 72rem;
    border-collapse: collapse;
    margin-top: var(--s3);
}

.portal-closing-days .page-head,
.portal-closing-days .lead {
    max-width: 72rem;
}

.portal-branch table.trail-rows th,
.portal-branch table.trail-rows td,
.portal-closing-days table.trail-rows th,
.portal-closing-days table.trail-rows td,
.portal-closing-days table.closure-rows th,
.portal-closing-days table.closure-rows td {
    border-bottom: 1px solid var(--border);
    padding: var(--s2) var(--s3) var(--s2) 0;
    text-align: left;
    vertical-align: top;
}

.portal-branch table.trail-rows th,
.portal-closing-days table.trail-rows th,
.portal-closing-days table.closure-rows th {
    color: var(--muted);
    font-size: var(--t-small);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .04em;
}

/* ⚠ CENTRED SINCE SAAS-355, and the reason is that this strip now holds two controls of DIFFERENT
   HEIGHTS. The destructive link is a bordered, padded box and the renaming one beside it is a bare
   link, so a stretching strip left "Rename…" four pixels above the text in the pill next to it —
   two things that should read as one row of actions, not quite on a line. `center` is also what
   the control treatment declares inside each box, so the strip and its contents now agree. */
.portal-closing-days .row-actions {
    display: flex;
    align-items: center;
    gap: var(--s2);
    flex-wrap: wrap;
}

/* The trail is four columns of prose and two of stamps; on a phone it scrolls inside its own box
   rather than pushing the page sideways — the freshness table's rule, and its reason. */
@media (max-width: 640px) {
    .portal-branch table.trail-rows,
    .portal-closing-days table.trail-rows {
        display: block;
        overflow-x: auto;
        white-space: nowrap;
    }
}
/* --- THE SYNC-FRESHNESS MARKER, BESIDE THE CLINIC IT IS ABOUT (SAAS-368) -----

   Darius ruled the freshness paragraph off Billing and the Appointments list on 2026-09-18 — "there
   are warnings like [it] which restrict readability for that page" — and ruled the replacement as
   "a marker that can be moused over to see the body". The marker lives inside the picker's own
   label, beside the clinic's name.

   ⚠ THE MARKER CARRIES THE WARNING ON ITS OWN, and that is what these rules are for rather than the
   tooltip. The sentence it replaced existed to stop a reader taking absent data for absent business,
   and it is now behind a gesture — so a reader who never makes that gesture has only this to go on.
   A bare `!` is not enough: a glyph beside a name is not read as "the numbers below may be
   incomplete". So the marker says a WORD, in the warning colour, at weight, at every width. Nothing
   here is decoration; it is the whole of the defence.

   NO SCRIPT, so neither page gains an `@rendermode`. The reveal is `:hover` for the pointer Darius
   asked for and `:focus-visible` for the two gestures he did not mention and did not exclude — Tab
   on a keyboard, a tap on a phone, both of which focus a `tabindex="0"` element. A native `title`
   would have served the mouse alone.

   ⚠ THE MARKER IS A SIBLING OF THE LABEL AND NOT A CHILD OF IT, so the row is what lays the two
   out. A click anywhere in a <label> toggles its control, so a marker nested in one would untick
   the clinic the moment a reader tapped it to read the sentence — which on a phone is the only way
   to read it at all. The row is therefore the flex container.

   ⚠ AND THE ROW WRAPS, WHICH IS SAAS-375 (measured, not guessed). Billing draws this picker in ONE
   `.ledger-filters` cell — 242 px at 1440 — and the marker is 94 px of it. Without wrapping, the
   marker and the name fight over a row that cannot hold both, and the name loses: "Malabar 21" on
   one line and "Dental" on the next, which is what Darius photographed. Flex wrapping is settled on
   max-content sizes BEFORE any shrinking, so the marker drops to its own line exactly when the pair
   will not fit, and stays beside the name whenever the row is wide enough — Appointments at 1440,
   where a one-Branch picker's row is the whole form. "The marker on its own line" is the first of
   the three remedies the ticket sanctions.

   NOT FLOORED AT 44 px, and that is this stylesheet's own boundary rather than an omission. The
   coarse-pointer block floors navigation and controls and deliberately leaves form fields and
   their labels at the density the picker was designed at — "a 44px floor on filter labels would
   treble the height of an eleven-Branch picker". The marker lives on one of those rows, so it
   keeps that density; what makes it reachable is the PADDING on the marker rather than the box
   around it, since SAAS-380 took the word away (see `.stale-marker`).

   ⚠ AND THE ROW IS THE TOOLTIP'S ANCHOR AGAIN (SAAS-380), which SAAS-375 removed on purpose — "left
   in place it would be an invitation to hang the next layer off the row and rediscover this
   ticket". The invitation was taken, and the ticket does not come back, because what made a layer
   here a defect was the list's own height cap and the cap is gone. A layer anchored to a row inside
   a list that neither scrolls nor clips has nothing to be clipped by. The reason is worth keeping
   next to the declaration: this is the property that decides whether the sentence is a tooltip or
   a block, and it reads like a stray one. */
ul.branches-select li {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2);
    position: relative;
}

/* The label takes the row's width so the clinic's name and the marker do not fight over it: the
   name is what a reader scans down, and the marker is what stops them. */
ul.branches-select li > label {
    flex: 1 1 auto;
    min-width: 0;
}

.stale-marker {
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
    color: var(--bad);
    font-weight: 600;
    font-size: var(--t-small);
    /* Focusable, so it must show where the focus is — the product's own ring rather than the
       browser's default, which a `display: inline-flex` box draws around its text only. */
    border-radius: var(--r-sm);
    cursor: help;
    /* ⚠ PRESSABLE, NOT MERELY DRAWN (SAAS-380). Darius took the word away, and the word was the
       marker's hit area as much as its meaning: alone, the badge is 1.15rem — about 18px — and on
       a phone a tap on it is the ONLY way to read the sentence at all. This picker is deliberately
       outside the coarse-pointer 44px floor, because flooring a filter label would treble the
       height of an eleven-Branch picker, so the floor will not do this for us.

       The padding is therefore the remedy and it is asymmetric on purpose: 10px of slack each side
       takes the target to 38 x 26 where the badge alone is 18 x 18, and the 4px top and bottom keep
       the ROW at the density the picker was designed at rather than growing every clinic's line by
       20px to make one badge square. Wider than tall is the right shape here — a finger travelling
       down a list of clinic names is off by more sideways than it is vertically.

       ⚠ AND NO NEGATIVE MARGIN TO CLAW THE SLACK BACK. The first draft pulled 4px off each side to
       keep the badge flush with the row's edge; measured at 1440 the marker's box then ran to 286px
       inside a 282px list and `overflow-x: clip` cut the difference off. Four pixels of padding,
       silently clipped, by a declaration nobody would connect to the rule that did it — which is
       SAAS-375's defect in miniature, one ticket later. The slack stays where it was put. */
    padding: 4px 10px;
}

.stale-marker:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
}

/* The glyph is round and filled, so it reads as a badge rather than as punctuation somebody typed
   into the clinic's name. Fixed width both ways, because a circle that is 1px wider than tall reads
   as a mistake. */
.stale-marker-glyph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.15rem;
    height: 1.15rem;
    border-radius: 50%;
    background: var(--bad);
    color: var(--surface);
    font-size: var(--t-small);
    line-height: 1;
}

/* THE WORD IS GONE (SAAS-380) and `.stale-marker-word` with it. What it said is now the marker's
   own `aria-label`, so the badge still announces itself to a screen reader; what it bought a
   sighted reader — the meaning without a pointer — is what Darius chose to give up. His call, and
   the argument it overturns is kept in BranchStaleMarker.razor rather than here. */

/* THE SENTENCE, AND THE WHOLE OF SAAS-375's MEASURED CAUSE.
   =========================================================================
   It used to hang under the row as `position: absolute`, hidden by `visibility`. Both of those
   were wrong, and the measurement says so rather than an opinion. On a ONE-Branch Tenant at 1440,
   with the same markup and only the tip removed:

       tip present:  list 242 x 29 visible, 336 x 126 scrollable  -> BOTH scrollbars
       tip absent:   list 257 x 29 visible, 257 x  29 scrollable  -> NEITHER

   A `visibility: hidden` box is still laid out, and an absolutely positioned box contributes to
   the scrollable overflow of every ancestor up to its containing block. The row was that
   containing block and the list is a scroll container, so a 336 x 93 tip nobody could see made a
   242 x 29 list scroll in both axes. The marker did not merely reveal an overflow that was already
   there: without it there is none at all.

   AND THE POPUP NEVER WORKED HERE ANYWAY. Measured at 375 on a one-Branch Tenant, the revealed tip
   sat entirely below the list's own 34 px box — nought pixels of it inside — so `overflow: auto`
   clipped every word of it. The reader could only have reached the sentence by scrolling the list,
   which takes the pointer off the marker that reveals it. SAAS-368's "the sentence is reachable by
   pointer" did not hold on a Tenant with one clinic.

   ⚑ AND IT IS A LAYER AGAIN SINCE SAAS-380, WHICH IS NOT A REVERSAL OF THAT — IT IS WHAT TAKING
   THE PANE AWAY BOUGHT. Darius, 2026-09-19, shown the sentence sitting in the row: "put this
   inside a mouse over". He is asking for a tooltip, which is what SAAS-368 said it was and what
   SAAS-375 could not deliver.

   READ THE MEASUREMENT ABOVE AGAIN AND IT SAYS WHY. "An absolutely positioned box contributes to
   the SCROLLABLE OVERFLOW of every ancestor" — scrollable overflow is a thing a SCROLL CONTAINER
   has. The list was one, because it capped its own height. It is not one any more: no
   `max-height`, no `overflow` on either axis, so there is no scrollable overflow for a layer to
   charge itself to and nothing to clip it. Both halves of SAAS-375's defect were the cap's, and
   the cap is what Darius removed one ticket later.

   IN FLOW BETWEEN THOSE TWO TICKETS, and the in-flow shape had a real cost he could see: revealing
   the sentence pushed every clinic below it down the page, so reading one clinic's state moved the
   others under the pointer. A layer leaves the list exactly where it was.

   THE ROW IS THE ANCHOR, through `position: relative` on the `li` — the same shape `.menu-pop` and
   the year pickers use, at their layer rather than a new one. `min-width: 100%` so it is never
   narrower than the row it belongs to, `max-content` so a short sentence is not stretched, and a
   ceiling that is the smaller of 26rem and the viewport less its gutters, so a long one cannot
   push the page sideways on a phone.

   HIDDEN BY `display: none`, AND THE OLD RULE'S REASON FOR REFUSING IT WAS FALSE. It said a
   `display: none` tip is not in the accessibility tree, so `aria-describedby` would name something
   a screen reader cannot read out. But accname's step 2A skips a hidden node ONLY when it is not
   directly referenced by `aria-labelledby` or `aria-describedby`; a node that IS directly
   referenced is traversed however it is hidden. `visibility: hidden` removes a node from the
   accessibility tree exactly as `display: none` does, so the old rule bought nothing with the
   layout it cost. What `display: none` buys is a box that is not laid out at all — which is the
   only state that contributes nothing to the list's scrollable overflow. */
.stale-marker-tip {
    display: none;
    position: absolute;
    top: calc(100% + var(--s1));
    left: 0;
    z-index: 20;
    flex-direction: column;
    gap: var(--s2);
    /* ⚠ AND THE PADDING COUNTS INSIDE THE WIDTH. This sheet sets `border-box` per rule rather than
       once on `*`, so without it the floor and the ceiling below are CONTENT widths and the panel
       comes out 26 px wider than it was asked to be — measured at 375 while the tip was in flow: a
       301 px row holding a 327 px sentence, which was the very overflow SAAS-375 was about, one
       property later. */
    box-sizing: border-box;
    width: max-content;
    min-width: 100%;
    /* ⚠ THE CEILING IS WHAT KEEPS A TOOLTIP FROM PUSHING THE PAGE SIDEWAYS. 26rem is a comfortable
       measure for a two-sentence notice; `100vw` less three gutters is the phone, where the row
       already starts inboard of the edge. Take the smaller, always. */
    max-width: min(26rem, calc(100vw - var(--s7)));
    padding: var(--s2) var(--s3);
    border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent);
    /* Opaque, and lifted at the layer this sheet already uses for in-page popovers. A tooltip over
       a list of clinic names must not let the names show through it. */
    background: var(--surface);
    box-shadow: var(--e2);
    color: var(--ink);
    border-radius: var(--r-sm);
    font-weight: 400;
    font-size: var(--t-small);
    text-align: left;
    white-space: normal;
}

/* THE SAME THREE GESTURES SAAS-368 RULED, through the adjacent sibling rather than through
   descent: the tip is now the marker's sibling in the row instead of its child, because a child of
   an `inline-flex` marker cannot take the ROW's full width. `:hover` is the pointer Darius asked
   for, `:focus-visible` is Tab, and `:focus-within` is the tap — a touch tap focuses a
   `tabindex="0"` element without matching `:focus-visible`, so that third selector is what makes
   the sentence reachable on a phone and is not a duplicate of the second. */
.stale-marker:hover + .stale-marker-tip,
.stale-marker:focus-visible + .stale-marker-tip,
.stale-marker:focus-within + .stale-marker-tip {
    display: flex;
}

/* =================================================================================================
   THE LIST PAGES ON PAPER (SAAS-502): Billing, Appointments and Patients

   Two rules, and each is a defect seen by printing the page through the print-proof harness at A4
   portrait (`PrintedSheets`, `DataFirstListsTests.ListsOnPaper*`) -- not a guess about paper.

   1. THE TABLE IS ON THE SHEET, ALL OF IT. Three things clipped a printed list's right-hand columns
      and said nothing on the sheet about it. The scroll box (`.ledger-scroll`, `.register-scroll`)
      is `overflow: auto`, which on paper is a clip, not a scrollbar. The table was
      `width: max-content`, so it was as wide as its text wanted (2,060 px for twelve receipt columns on
      a 718 px sheet) and the box cut it at the edge. And every cell was one line, the remark's
      ellipsis included. Measured before this block, columns whole on an A4 portrait sheet: Billing
      receipts 3 of 12 (Clinic, Receipt no., Issued; the Patient column cut mid-name), payments 4 of 8,
      deposits 3 of 9, the Appointments list 3 of 8, and the Patients register across several clinics 5 of 7
      (Billing scheme and Registered gone; one clinic's register fits). A4 LANDSCAPE ALONE DID NOT FIX THEM:
      receipts 6 of 12, payments 6 of 8, deposits 6 of 9, Appointments 5 of 8, because the table stayed
      as wide as its text.

      So the boxes open; the table takes the sheet's width and not its text's (`width: 100%`); its
      text is set at 12 px (9 pt; SAAS-502 built it at 10 px, 7.5 pt, on a portrait sheet, and SAAS-529
      raised it when the lists went landscape, 3 below) and wraps. SAAS-458 refused 6.2 pt for a
      calendar's legend and its floor is 6.6 pt (a calendar day is zoomed to no less than 0.8 of 11 px =
      6.6 pt); the smallest text of a printed list is the 11 px heading, 8.25 pt, which is above it. A
      figure, a date and a receipt number never wrap (a split "1,250" / ",000" is a wrong number read
      right); every other cell may, and `overflow-wrap: anywhere` is what keeps a long unbroken token
      from pushing the table past the sheet again -- it sets a floor of one character for a cell instead
      of the whole word, so the table can always narrow to the page. The cost is where a column gets
      narrow: a long word in a 55 px column breaks inside itself, which on a portrait sheet was the
      twelve-column receipts list at its worst ("Muhamma / d", "Corporat / e").

      The remark was one line with an ellipsis at 26 rem on screen, with the whole note on the cell's
      title, which paper has no pointer to show. On paper it wraps, in a column held to 12 em: the
      table shares what is left of the sheet among its cells in proportion to what each asks for, and a
      240-character note left to ask for its whole length takes the width of the cells beside it.

      The heading repeats on every sheet the table runs onto and a row never splits across two
      (`break-inside: avoid`, the day-end's rule above). The sticky heading becomes an ordinary one:
      sticky has nothing to stick to on paper.

   2. CONTROLS DO NOT PRINT, STATE DOES (Darius on the calendars, SAAS-482/497: "Hide controls"; the
      coordinator extended it to these pages on 2026-10-09). Hidden: the Focus-on picker, the filter
      strip (quick dates, the page's primary search, the Advanced filter, the date range, and the chips
      with their x and "Clear filters"), the export link, the sort control, the pager's step and page
      links, and Appointments' "Open the calendar" link. Kept: the sentence "Narrowed to ...", which
      states EVERY set filter in words (measured with every filter set on all six views: each chip's
      filter has its clause), the pager's count and "Page 1 of N", each clinic's count, and the Practice
      band's sentence. So the chips are not printed even without their x: the sentence above the table
      already says what they say, and a second, shorter statement of the same filters beside it would
      be one more place for the two to differ ("Raised by: Kasir Front Desk" against "raised by
      "frontdesk"").

   3. THE LISTS PRINT ON A4 LANDSCAPE (SAAS-529; Darius, 2026-10-09: "Landscape", shown the receipts
      list cut off, at A4 portrait at 7.5 pt with names broken mid-word, and at A4 landscape). A
      NAMED PAGE, `lists`, as the calendar week's is (`@page calendar-week`, above), and like it
      NAMING AN ORIENTATION AND NO PAPER: `size: landscape` turns whatever paper the printer holds, so
      an A4 clinic prints A4 and a Letter one Letter. A paper size here is refused by the week's own
      stylesheet test.

      WHERE IT IS GIVEN: to `body`, when the page holds a list, and not to the list's section. The
      Practice band, whose sentence prints, sits in the layout OUTSIDE the section; a named page on
      the section alone made a page break between the band and the list and printed the band by itself
      on a portrait sheet ahead of the table (measured on a reader focused on a colleague). The
      calendars have no `.portal-billing` or `.portal-patients`, so a calendar day stays portrait and
      a week keeps `calendar-week`. The Appointments list is a `.portal-billing` page.

      THE SIZE: 12 px, 9 pt, the largest whole pixel size at which no word breaks inside itself. That
      was measured and not assumed, on the receipts list (the widest, twelve columns) set with the
      longest words real names run to: a patient "Purnamasari Wijayakusuma", a scheme "Internasional",
      a clerk "Administrasi", a doctor "Christopher Wijaya Kusuma, Sp.Ort". At 12 px nothing breaks; at
      13 px "Administrasi" and "Internasional" do; at 14 px a phone number breaks at its hyphens too.
      Between 10 and 12 px the sheet gains only a line or two a row, because landscape has the room.
   ⚠ THIS IS NOT THE SHEET'S LAST `@media print`, ON PURPOSE: `BranchStaleMarkerTests` and `BranchPickerScrollTests` read the LAST
   print block in the sheet for `.stale-marker-tip` (the block under "PAPER HAS NO POINTER", below), so this one stands before it.
   Everything it overrides is earlier in the sheet or less specific, and `DataFirstListsTests.ListsOnPaper*` holds that it wins.
   ================================================================================================= */

@page lists {
    size: landscape;
}

@media print {
    body:has(.portal-billing, .portal-patients) {
        page: lists;
    }

    /* `a.clear-number` is Billing's "Clear the number" (SAAS-472), the control that goes with a receipt number's matches: a link, as the pager's pages are,
       and nothing a sheet can follow (SAAS-502's list of controls did not name it; SAAS-561 and SAAS-562). */
    :is(.portal-billing, .portal-patients) :is(.practice-switch, .filter-strip, .export, .sort-control, .pager-pages, .block-link, a.clear-number) {
        display: none;
    }

    :is(.portal-billing, .portal-patients) :is(.ledger-scroll, .register-scroll) {
        overflow: visible;
        max-height: none;
    }

    :is(.portal-billing, .portal-patients) :is(table.ledger, table.register) {
        width: 100%;
        min-width: 0;
        font-size: 12px;
        line-height: 1.25;
    }

    :is(.portal-billing, .portal-patients) :is(table.ledger, table.register) :is(th, td) {
        padding: 3px 4px;
        white-space: normal;
        overflow-wrap: anywhere;
    }

    :is(.portal-billing, .portal-patients) :is(table.ledger, table.register) thead {
        display: table-header-group;
    }

    :is(.portal-billing, .portal-patients) :is(table.ledger, table.register) thead th {
        position: static;
        font-size: 11px;
        letter-spacing: 0;
        overflow-wrap: normal;
    }

    :is(.portal-billing, .portal-patients) :is(table.ledger, table.register) tr {
        break-inside: avoid;
    }

    /* THE COUNTS ARE PRINTED ABOVE THE TABLE, NOT BELOW IT (SAAS-536). A list ends in its counts: the pager's "1-18 of 18 receipts" and "Page 1 of 1",
       and under them each clinic's own ("Klinik Kidz Dental Sehati 6 receipts"). On paper the last of them could sit alone on a sheet of its own (the
       grouped receipts on A4, the deposits of one clinic on A4, receipts and Appointments on F4: one clinic's count and nothing else), and the cause
       is not a rule this sheet lacks. MEASURED, in isolation and in these pages: Chromium honours `break-before`, `break-after` and `break-inside:
       avoid` to keep a block that follows a table with the table's last row only where the block starts beyond the sheet's foot. A block that
       STRADDLES the foot is split between two lines whatever is asked: the same three table lengths in every sixteen fail, with the counts as a list,
       as one box, as a flex box, with and without margins, with the table's rows allowed to split, as a `tfoot` and as a `caption`. A row INSIDE the
       table's body with `break-before: avoid` never fails, and that is the pager and the counts drawn inside every list's table, which is the
       `tbody tr` of 38 test files and a pager that would scroll with the table on a phone. So the counts go above it, where nothing can leave
       them behind: the page is a flex box on paper (a column; a wrapping row since SAAS-544, below) and the table, which comes first in the markup, takes the
       last place. Only paper: a screen is as it was. A clinic's own block on the Appointments list (`.branch-block`) was the same until SAAS-562, which found that a wrapping
       flex box does not keep its first line with its second (below), and it is a block with its count at the top now; the Day End's blocks (`.dayend-block` is
       a `.branch-block` too) are not touched: they are under `.portal-dayend`. */
    :is(.portal-billing, .portal-patients) {
        display: flex;
    }

    :is(.portal-billing, .portal-patients) > :is(.ledger-scroll, .register-scroll) {
        order: 1;
    }

    /* A flex column does not collapse its children's margins, as a block does. The filter form is a box with nothing printed in it and a margin under
       it that a block let run into its neighbours, and in the column it is 16 px of the sheet: enough to take a list that just fitted Letter (the deposits
       of one clinic) to a second sheet. Measured: every Billing and Appointments page was 16 px taller. */
    :is(.portal-billing, .portal-patients) > form.filter-form {
        margin: 0;
    }

    /* A LIST'S HEADER IS THREE SHORT LINES, NOT A THIRD OF THE FIRST SHEET (SAAS-544). Darius, 2026-10-10: "the header is inefficient, took too many spaces", shown a mock on the
       grouped receipts and asked whether to build it as shown: "yes". On A4 landscape everything above a list's column headings took 231 to 391 px of an 794 px sheet
       (measured on `9433c00f`, the sheet's margin included): the title at its screen size, the sync line, the opening-range hint in two lines, the "Narrowed to" sentence,
       the pager's count and "Page 1 of 1" each on a line of their own, and one padded row for each clinic's count. The grouped receipts printed four sheets for it, and
       print three with it. What is left is three lines, measured at 101 to 105 px from the sheet's top (the 37 px margin is part of it): (1) the title in 17 px and the sync line beside it; (2) the "Narrowed to" sentence and the
       pager's count beside it; (3) each clinic's count, on one line wherever they fit. Paper only: a screen is as it was.

       THE PAGE IS A WRAPPING ROW, not the column SAAS-536 made of it. Every child is a line of its own (`flex: 0 0 100%`, which is what a column was) except the few that share
       one: the title and the sync line, the sentence and the count, the Patients register's scope and its count. The filter form, which has nothing printed in it, is what ends
       the title line: it is a full line with no height. Items keep the order they have (the table last, SAAS-536), so the count, which stands after the table in the markup, comes
       under the sentence.

       THE RANGE IS STATED ONCE. "It opens on today — pick another range. One date on its own reads open-ended; both cannot be empty." is a hint to a reader who could pick a
       range, and where the "Narrowed to" sentence reads the range back ("issued 2026-08-05 to 2026-08-05") it says it twice, in two lines. Measured in every view as it opens and
       as it is narrowed (receipts, payments, deposits, the Appointments list in its two shapes): the sentence is always there, because a Billing or Appointments list opens on
       today and says so. A view that printed no sentence keeps the hint, in the small type (the rule is `:has(> p.narrowed)`, so it follows the markup and not a list of views).

       "PAGE N OF M" IS PRINTED WHERE THERE IS MORE THAN ONE PAGE: the pager marks a set of one page (`pager-one-page`, markup only, a screen has no rule for it), and "Page 1 of 1"
       says nothing "1-18 of 18" has not.

       A CLINIC'S HEADING ROW NEVER ENDS A SHEET. In the mock, "Klinik Gigi Warung Jambu Utama · all 6 matching receipts" stood at the foot of sheet 1 and its receipts began sheet 2.
       Measured on the grouped receipts at every margin from 0.2 to 0.9 inch in steps of 0.05: without a rule it was the last line of a sheet at 0.40, 0.45 and 0.55 (and
       on `9433c00f` at 0.25, 0.75, 0.80 and 0.85). Either `break-after: avoid` on the heading row or `break-before: avoid` on the row after it holds at all fifteen, as SAAS-536
       measured for a row inside a table's body; both are written, so that whichever a browser reads is there. */
    :is(.portal-billing, .portal-patients) {
        flex-flow: row wrap;
        align-items: baseline;
        column-gap: 14px;
        row-gap: 0;
    }

    :is(.portal-billing, .portal-patients) > * {
        flex: 0 0 100%;
    }

    :is(.portal-billing, .portal-patients) > h1 {
        flex: 0 0 auto;
        margin: 0;
        font-size: 17px;
        line-height: 1.3;
    }

    /* The sync line: Billing's and Appointments' `copy-synced`, and the Patients register's sync mark, whose sentences print (SAAS-469) as a head and a notice. */
    :is(.portal-billing, .portal-patients) > :is(p.copy-synced, .sync-mark) {
        flex: 0 1 auto;
        margin: 0;
        font-size: 10.5px;
    }

    :is(.portal-billing, .portal-patients) > .sync-mark .info-mark-panel {
        margin: 0;
        padding: 0;
        border: 0;
        background: none;
    }

    :is(.portal-billing, .portal-patients) > .sync-mark :is(.sync-mark-head, p.stale-notice) {
        display: inline;
        margin: 0;
    }

    /* A copy's notice can say two things, the Agent's last word and a Surface it never measured, and they are two paragraphs. Inline, as the rule above draws them,
       the second ran on from the first with no space between ("...'da_sync'@'10.8.0.14'How current ... is not measured yet.", fresh-eyes review 9, reviewer D). The
       first sentence stays beside the head, as SAAS-544 drew it; each one after it begins a line, as the paragraphs did before SAAS-544 (SAAS-561). */
    :is(.portal-billing, .portal-patients) > .sync-mark p.stale-notice + p.stale-notice {
        display: block;
    }

    /* A lead: the Appointments list's "Nobody else's bookings are here ..." is state, in the small type and on the title's line. */
    :is(.portal-billing, .portal-patients) > p.lead:not(.own-rows-only) {
        flex: 0 0 auto;
        margin: 0;
        font-size: 10.5px;
    }

    :is(.portal-billing, .portal-patients):has(> p.narrowed) .opening-range {
        display: none;
    }

    :is(.portal-billing, .portal-patients) > h2.branch-name {
        flex: 0 0 auto;
        margin: 0;
        font-size: 12px;
        line-height: 1.3;
    }

    :is(.portal-billing, .portal-patients) > p.narrowed {
        flex: 0 1 auto;
        margin: 0;
        font-size: 10.5px;
    }

    /* A RECEIPT NUMBER'S MATCHES (SAAS-472) HAVE THE COMPACT HEADER TOO (SAAS-561). SAAS-544 measured every list as it opens and as it is narrowed and did not measure
       this one: the column headings stood 185 px from the sheet's top (the bound is 110), and "every date" was said twice, once in the heading over the matches
       ("Receipt number A00 - every date - 18 receipts") and once in the sentence ("Narrowed to: receipt number starts with "A00", on every date."). The heading says nothing
       the sentence and the pager's count do not (the number, every date, how many), so on paper it is not printed and the range is said once; what is left of the
       block is its link, which is a control (`a.clear-number`, above), so the block has no height. "Newest first while a receipt number is set" is state (a number fixes
       the order of its matches) and stays, in the small type, on the sentence's line. */
    :is(.portal-billing, .portal-patients) > .number-found {
        margin: 0;
    }

    :is(.portal-billing, .portal-patients) > .number-found > .number-found-heading {
        display: none;
    }

    :is(.portal-billing, .portal-patients) > p.sort-fixed {
        flex: 0 1 auto;
        margin: 0;
        font-size: 10.5px;
    }

    :is(.portal-billing, .portal-patients, .portal-billing .branch-block) > nav.pager {
        flex: 0 0 auto;
        margin: 0;
        padding: 0;
        border: 0;
        font-size: 10.5px;
    }

    :is(.portal-billing, .portal-patients, .portal-billing .branch-block) > nav.pager p {
        margin: 0;
        font-size: 10.5px;
    }

    :is(.portal-billing, .portal-patients, .portal-billing .branch-block) > nav.pager.pager-one-page .pager-where {
        display: none;
    }

    /* The deposits' balances (what each branch holds now) stand between the counts and the table, as they stood above the table's rows. */
    :is(.portal-billing, .portal-patients) > .held-per-branch {
        order: 1;
    }

    :is(.portal-billing, .portal-patients) > ul.merged-sites {
        display: flex;
        flex-wrap: wrap;
        column-gap: 18px;
        row-gap: 0;
        margin: 3px 0 6px;
        padding: 0;
        border: 0;
    }

    /* A CLINIC'S COUNT LINE MAY WRAP WITHIN THE SHEET (SAAS-561). It was `flex: 0 0 auto`, a line sized to its whole width that never shrinks, and a refused
       clinic's line holds the driver's own message (255 characters in the longest seen) and a capped one holds SAAS-472's sentence beside a clinic name of
       fifty-six. A line wider than the sheet is not wrapped by Chromium but answered with a smaller sheet: the whole page is shrunk to fit it (A4 74.5%, Letter
       69.8%, F4 83.4%, A5 66.7% with the words cut), and the 10.5 px header printed at 5.9 pt, under SAAS-458's floor. Measured by fresh-eyes review 9 (reviewer D):
       `flex: 0 1 auto; min-width: 0` returns A4, Letter and F4 to 100%, and A5 to the scale its list has without the line. The line is a wrapping flex box of its own
       (the screen rule): its sentence stays beside the clinic's name and wraps there (`.merged-site-said`), as fresh-eyes review 10 measured. */
    :is(.portal-billing, .portal-patients) > ul.merged-sites > li {
        flex: 0 1 auto;
        min-width: 0;
        gap: 6px;
        padding: 0;
        border: 0;
        font-size: 10.5px;
    }

    :is(.portal-billing, .portal-patients) > ul.merged-sites .merged-site-name {
        min-width: 0;
    }

    /* A CLINIC'S BLOCK ON THE APPOINTMENTS LIST KEEPS ITS HEADING WITH ITS FIRST ROW (SAAS-562). On paper a block's name and count, and its column headings, could be the last
       thing on a sheet with its first row on the next, and no clinic's name over the rows (fresh-eyes review 9, reviewer D: thirty clinics, A4 sheets 2 and 9 and F4 sheet 1; it was
       so before SAAS-544 as well). MEASURED, in these pages and in isolation (thirty blocks of a heading, a count and a table, A4 landscape): Chromium does not honour
       `break-after: avoid` between the lines of a WRAPPING flex box, nor between the rows of a grid (the block was a wrapping flex box: its name and count on a line, its table under them);
       it honours it between the items of a flex COLUMN and between block-level boxes. `break-inside: avoid` on the whole block holds, and costs sheets where blocks are of a middling
       length (twelve rows a block: 30 sheets where 18 were enough). So the block is a plain block: its name, then its table, with `break-after: avoid` on the name and
       `break-before: avoid` on the table's first row (a row inside a table's body, as SAAS-544 held a group heading row). The column headings need no rule of their own: with the row's rule in place,
       `break-after: avoid` on them as well changed nothing at nineteen margins and papers, and without it four sheets end on them. Its count, which stands after the
       table in the markup, is at the top right of the name's line by position (it is out of the flow, so it takes no line of its own and needs no flex `order`), and the name leaves
       room for it. A block that begins at a sheet's foot moves, name and all, to the next sheet. */
    .portal-billing .branch-block {
        position: relative;
    }

    .portal-billing .branch-block > h2 {
        margin: 0;
        padding-right: 20em;
        font-size: 13px;
        line-height: 1.3;
        break-after: avoid;
    }

    .portal-billing .branch-block > nav.pager {
        position: absolute;
        top: 0;
        right: 0;
        line-height: calc(13px * 1.3);
    }

    .portal-billing .branch-block table.ledger tbody tr:first-child {
        break-before: avoid;
    }

    table.ledger tr.group-head {
        break-after: avoid;
    }

    table.ledger tr.group-head + tr {
        break-before: avoid;
    }

    /* What is a number or an identifier is not broken: a figure, a date, a receipt number, a phone number (SAAS-529: with the longest
       names in the register's other columns the phone column was squeezed and "0812-5551-0010" broke at its hyphens). */
    :is(.portal-billing, .portal-patients) :is(table.ledger, table.register) :is(th, td).num,
    :is(.portal-billing, .portal-patients) table.ledger td.receipt-no,
    :is(.portal-billing, .portal-patients) table.ledger td:is([data-col="issued"], [data-col="taken"], [data-col="moved"], [data-col="when"]),
    :is(.portal-billing, .portal-patients) table.register td:is(.phone, [data-col="birthday"], [data-col="registered"]) {
        padding-inline-start: 4px;
        white-space: nowrap;
    }

    /* Rules above that name a cell by class and set it to one line on screen. */
    :is(.portal-billing, .portal-patients) :is(table.ledger, table.register) :is(th, td).clinic {
        white-space: normal;
    }

    :is(.portal-billing, .portal-patients) table.ledger :is(th, td).performedby {
        min-width: 0;
    }

    /* A state is one word of a short fixed vocabulary (Paid, Unpaid, Scheduled): it wraps between words, never inside one. */
    :is(.portal-billing, .portal-patients) table.ledger td.state {
        overflow-wrap: normal;
    }

    :is(.portal-billing, .portal-patients) table.ledger td.remark > span {
        max-width: 12em;
        overflow: visible;
        white-space: normal;
    }

    :is(.portal-billing, .portal-patients) table.ledger td.movement .movement-kind {
        font-size: inherit;
    }
}

/* PAPER HAS NO POINTER (SAAS-368's Done-when). The tip prints as text beside the clinic's name
   rather than vanishing, which is the whole of what "or if it does, it prints as text rather than
   as a hover the paper cannot deliver" asks for.

   ⚠ AND IT COMES BACK INTO FLOW TO DO IT (SAAS-380). `display: flex` alone is no longer enough:
   now that the tip is a LAYER, unhiding it on paper would print one absolutely positioned panel on
   top of the clinic names below it, and on a many-clinic reading every panel would stack in the
   same place. `position: static` is what turns a tooltip back into a paragraph, and the shadow and
   the ceiling go with it — paper has no elevation and no viewport. */
@media print {
    .stale-marker-tip {
        display: flex;
        position: static;
        width: auto;
        min-width: 0;
        max-width: none;
        margin-top: var(--s1);
        box-shadow: none;
    }
}

/* =================================================================================================
   THE PHONE'S ONE BAR, AND A CALENDAR'S BOTTOM BAR (SAAS-468, ADR 0027's third addendum).

   Darius, 2026-10-02 about 23:35, on his phone, on Malabar 21 Dental's calendar as a Practising Account
   focused on a colleague: "it doesnt even show the schedule in the first page, too much space is taken
   by the header." Measured on his screenshot, the schedule's first row sat about 787px down — below the
   screen. Four layouts were drawn with his real content and HE CHOSE C ("C", 2026-10-02 about 23:55),
   asking that the bottom bar stay put while the schedule scrolls.

   BELOW 768px, AND ONLY THERE. Every rule from here to the end of this section is in a `screen and
   (max-width: 767.98px)` query, so a desktop is what SAAS-463 left it and PAPER IS NOT A PHONE
   (SAAS-451's lesson: a bare `max-width: 767px` matches a portrait A4 sheet). The one wide-screen rule
   is the opposite — it makes the calendar's new disclosure transparent at 768px and up, and on paper.

   1. ONE STICKY TOP BAR, 56px, on every page that has a menu: the Menu button, a two-line title (the page's
      own title, then the Practice focus or the Tenant's name), the sync mark where the page has one
      (SAAS-469), and the account. The masthead's row, the Practice band and SAAS-463's Menu bar all fold
      into it. The Menu button is the shell's own <summary>, fixed over the bar's left end; the open menu
      is a fixed panel under the bar, so it is where the reader is however far the page has scrolled — the
      SAAS-463 note chose "pushes the page down" for a bar that scrolled away, and that stopped being a
      choice the day the bar stuck.
   2. A CALENDAR'S CONTROLS ARE A FIXED BOTTOM BAR (‹ Today › Day/Week More), clear of the home indicator
      with `env(safe-area-inset-bottom)`, and everything else the page's rows held is in the sheet that
      More opens above it. The grid is its own scroller between the two bars, with the day and the column
      heads stuck to its top, and the page is padded by the bar's height so the last booking is never
      under it.
   ============================================================================================== */

:root {
    --bar: 56px;
    --cal-bar: 68px;
}

/* The wordmark of the open phone menu: not drawn at all where the masthead carries it. */
.nav-wordmark {
    display: none;
}

/* The phone bar's title block (MainLayout, BarFocus): drawn below 768px only. A wide screen's masthead is
   the wordmark and the account, and this block's words are in the page's own heading and the rail. */
.bar-title {
    display: none;
}

/* A thing the phone draws and a wide screen does not — the one-clinic calendar's second copy of By clinician /
   By room, and the calendars' second copy of the page's explanation (each copy is shown at one width and not the
   other, so a screen reader meets it once). Grid / Agenda is NOT here since SAAS-521: it is on the phone's bar,
   in the nav row's own pair, and has no copy in the sheet. */
.cal-phone-only,
.cal-axes.cal-phone-only {
    display: none;
}

/* THE NARROWING'S PHONE LINE (SAAS-491; Darius 2026-10-08, "Yes, show it above the grid"): whose rows a
   narrowed calendar holds, drawn above the grid below 768px only. A wide screen and paper have the
   narrowed sentence and the practised-only lead in the page, so the line is not drawn there and the fact
   is said once. */
.cal-narrowed-line {
    display: none;
}

/* The clip recipe `.visually-hidden` is, for the word a button keeps for a screen reader and does not
   draw. (The step words — "Previous", "Next" — are NOT here: they are drawn at a wide width and clipped
   in the phone block below.) */
.cal-bar-more-word {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.cal-bar-more > summary {
    list-style: none;
    cursor: pointer;
}

.cal-bar-more > summary::-webkit-details-marker {
    display: none;
}

/* WIDE, AND PAPER: the calendar's disclosure is transparent. `.cal-controls`, the <details>, and the
   sheet inside it draw no box of their own, so the controls sit in the page's flow exactly where they
   always did (SAAS-463's technique for the menu, behind the same feature query and for the same reason:
   a browser without `::details-content` keeps a More button and the controls behind it, which is worse
   than nothing wide but never loses them). Paper is a wide screen for this purpose — a portrait A4 sheet
   is under 768px. The controls themselves do not print (SAAS-497, in the calendar's print block); what
   this keeps in the page's flow on paper is what prints from among them: the date line and the doctor's
   lead paragraph. */
@media (min-width: 768px), print {
    @supports selector(::details-content) {
        .cal-controls,
        .cal-bar-more,
        .cal-bar-sheet {
            display: contents;
        }

        .cal-bar-more > summary {
            display: none;
        }

        .cal-bar-more::details-content {
            content-visibility: visible;
            display: contents;
        }
    }
}

@media screen and (max-width: 767.98px) {
    /* ---- 1. The top bar ------------------------------------------------------------------------ */

    .site-header.has-menu .masthead-inner {
        box-sizing: border-box;
        height: var(--bar);
        /* Room for the Menu button, which is the shell's own summary fixed over this end. */
        padding: 0 var(--s1) 0 calc(44px + var(--s2));
        gap: var(--s1);
        flex-wrap: nowrap;
    }

    /* The wordmark is in the open menu and the Tenant's name is line two, so the bar spends none of
       itself on either. */
    .site-header.has-menu .brand,
    .site-header.has-menu .site-tenant {
        display: none;
    }

    .bar-title {
        display: flex;
        flex: 1 1 0;
        flex-direction: column;
        justify-content: center;
        gap: 1px;
        min-width: 0;
    }

    .bar-line1,
    .bar-line2 {
        display: block;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        color: var(--on-brand);
    }

    .bar-line1 {
        font-size: 15px;
        font-weight: 650;
        line-height: 1.25;
    }

    /* The area's name stands in for a title the page did not hand up, and gives way the moment it did:
       the page's copy comes first in the line (the outlet), the area's second. */
    .bar-own ~ .bar-area {
        display: none;
    }

    .bar-line2 {
        font-size: 12px;
        font-weight: 500;
        line-height: 1.25;
    }

    /* LINE TWO, WHEN IT IS THE PRACTICE FOCUS, IS A DISCLOSURE: tapping it opens the Practice band under
       the bar. A summary has no marker of its own here, a chevron says it opens, and the target is the 44px
       the floor asks for although the line is 15px tall — the box grows over the bar's own blank space
       with a pseudo-element, so the line does not. */
    .bar-focus > summary.bar-line2 {
        position: relative;
        display: flex;
        align-items: center;
        gap: var(--s1);
        min-height: 0;
        list-style: none;
        cursor: pointer;
    }

    .bar-focus > summary.bar-line2::-webkit-details-marker {
        display: none;
    }

    .bar-focus > summary.bar-line2::before {
        content: "";
        position: absolute;
        inset: -16px 0;
    }

    .bar-focus > summary.bar-line2::after {
        content: "";
        flex: 0 0 auto;
        width: 5px;
        height: 5px;
        margin-top: -2px;
        border: solid currentColor;
        border-width: 0 2px 2px 0;
        transform: rotate(45deg);
        transition: transform 160ms ease;
    }

    .bar-focus[open] > summary.bar-line2::after {
        margin-top: 3px;
        transform: rotate(225deg);
    }

    /* THE MENU BUTTON: the shell's own <summary>, over the bar's left end. Its words stay for a screen
       reader and are not drawn; the closed bar is an icon on the bar, and the open menu hangs under it. */
    .shell-menu > summary {
        position: fixed;
        z-index: 41;
        top: 0;
        left: var(--s1);
        width: 44px;
        height: var(--bar);
        justify-content: center;
        padding: 0;
        background: none;
    }

    .shell-menu > summary .shell-menu-label,
    .shell-menu > summary .shell-menu-here,
    .shell-menu > summary .shell-menu-chevron {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
    }

    /* THE OPEN MENU, under the bar and over the page, scrolling inside itself. */
    .shell-nav {
        position: fixed;
        z-index: 39;
        top: var(--bar);
        left: 0;
        right: 0;
        box-sizing: border-box;
        max-height: calc(100vh - var(--bar));
        max-height: calc(100dvh - var(--bar));
        overflow-y: auto;
        box-shadow: var(--e2);
    }

    .nav-wordmark {
        display: flex;
        align-items: center;
        min-height: 44px;
        padding: 0 var(--s2);
        color: var(--ink-strong);
        font-weight: 650;
        letter-spacing: -.01em;
        text-decoration: none;
    }

    /* THE ACCOUNT: the chip alone, 44px. The name and the chevron are for a wide band; the name is the
       button's accessible name and stays, and the Tenant is in the menu it opens. */
    .site-header.has-menu .account-menu > summary {
        justify-content: center;
        min-width: 44px;
        min-height: 44px;
        padding: 0;
        border: 0;
    }

    .site-header.has-menu .account-menu > summary .account-name,
    .site-header.has-menu .account-menu > summary .glyph {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
    }

    /* THE PAGE'S SYNC MARK (SAAS-469), lifted into the bar: the dot and the time. It stays where the
       page put it in the document — beside the title — and is drawn over the bar, because a fixed box
       is positioned against the viewport whatever its place in the markup. The words "Copy synced" stay
       for a screen reader and for the panel's own heading. */
    .site-main .sync-mark {
        position: fixed;
        z-index: 41;
        top: 0;
        right: calc(44px + var(--s2));
        display: flex;
        align-items: center;
        height: var(--bar);
    }

    .site-main .info-mark-button.sync-mark-button {
        min-height: 44px;
        margin: 0;
        padding: 0 var(--s2);
        border: 0;
        background: none;
        color: var(--on-brand);
        font-size: 12px;
    }

    .site-main .sync-mark .sync-mark-lead,
    .site-main .sync-mark .sync-mark-glyph {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
    }

    /* A dot with a ring in the surface colour, so it reads on the bar's teal in both themes. */
    .site-main .sync-mark .sync-mark-dot {
        box-shadow: 0 0 0 2px var(--surface);
    }

    /* The panel opens under the bar across the page's own gutters, never wider than the screen. */
    .site-main .sync-mark .info-mark-panel {
        position: fixed;
        top: calc(var(--bar) + var(--s1));
        left: var(--s4);
        right: var(--s4);
        width: auto;
        max-width: none;
    }

    /* The sync mark's own row is not in the title's way: the title block stops short of it. The room is
       the widest label a stale copy draws ("2 Oct 15:07": the date is added the moment the last sync was
       not today, which is exactly the case where the mark is amber) — measured on a capture, where 76px
       let line two run under the dot. */
    @supports selector(:has(a)) {
        body:has(.site-main .sync-mark) .bar-title {
            padding-inline-end: 108px;
        }

        .bar-focus-text {
            min-width: 0;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        /* THE PAGE'S OWN h1 IS VISUALLY HIDDEN where its title is in the bar — still the document's one
           heading for a screen reader, which is why it is clipped and not removed; the bar's copy is
           aria-hidden. A page that hands up no title keeps its heading. */
        body:has(.bar-own) .site-main h1 {
            position: absolute;
            width: 1px;
            height: 1px;
            margin: -1px;
            padding: 0;
            overflow: hidden;
            clip-path: inset(50%);
            white-space: nowrap;
            border: 0;
        }

        /* THE PRACTICE BAND IS UNDER THE BAR ON DEMAND. Its state is line two of the bar; the band itself
           is what line two opens. Without `:has()` it stays in the page where it always was. */
        body:has(.site-header.has-menu .bar-focus) .practice-band {
            display: none;
        }

        body:has(.site-header.has-menu .bar-focus[open]) .practice-band {
            display: flex;
            position: fixed;
            z-index: 39;
            top: var(--bar);
            left: 0;
            right: 0;
            box-shadow: var(--e2);
        }
    }

    /* ---- 2. A calendar ------------------------------------------------------------------------- */

    /* The schedule starts under the bar. The page's own top padding and its explanation mark are gone:
       the explanation is the sheet's text here, and the title is the bar's. */
    .portal-calendar {
        margin-block-start: calc(var(--s5) * -1);
        padding-block-end: calc(var(--cal-bar) + env(safe-area-inset-bottom, 0px));
    }

    /* ONLY THE TITLE'S MARK (SAAS-483). The mark right after the h1 explains "this calendar", and its words
       are the More sheet's own text on a phone, so it is not drawn. It used to be every direct child
       `.info-mark` of the page, which also took the marks beside the count sentence ("About the grid" on the
       per-Branch calendar, "how the grid is drawn" and "how the agenda is drawn" on the merged one): their
       words are written once, behind the mark, and exist in no other place on the page. */
    .portal-calendar > h1 + .info-mark:not(.sync-mark) {
        display: none;
    }

    /* A healthy copy's plain line stays in the page; it just has to clear the bar it now sits under. */
    .portal-calendar > p.copy-synced {
        margin-block-start: var(--s2);
    }

    /* THE BOTTOM BAR: SIX BOXES ON ONE LINE, EACH 44 PX HIGH AND AT LEAST 44 WIDE (SAAS-521).

       ‹ Today › as one joined control on the left; on the right ONE button naming the other span (Week while the
       window is a day, Day while it is a week), ONE naming the other view (Agenda in the grid, Grid in the agenda),
       and More.

       WHY ONE BUTTON EACH, AND NOT TWO LINKS. Darius, 2026-10-09: "i cant find the way to get agenda in calendar" —
       Grid / Agenda was the first thing in the More sheet — and he ruled it goes on the bar of both calendars, in
       one tap, with every item a 44 px target and the bar on one line at 375 and at 320 px, accepting one button
       naming the other view where two links will not fit. THEY WILL NOT, AND MEASURING SAID BY HOW MUCH. The bar as
       it stood did not hold at 320 at all: Week was drawn UNDER More (the nav ran 58 px past its box and the two
       overlapped), because nothing had measured 320 until this ticket did; and at 375 it had room for six boxes and
       no more. Seven boxes cannot be 44 px wide in 304 px with a gap between any two (7 x 44 = 308), so the bar
       had to lose a box, and the cheapest loss keeps every control one tap away: below 768 px the link marked
       current is not drawn and the other is. It is the same two links in the same row, so a wide screen draws both
       of each pair untouched, the hidden one is not in the accessibility tree twice, and what the page is showing
       is on the page — the date line of the grid and the day heads of the agenda say day or week, grid or agenda.

       THE ARROWS AND TODAY ARE ONE JOINED CONTROL because that is what they are, and because it buys 10 px that
       three separate boxes would spend on gaps. The text is --t-small (12.5 px) with 5 px of padding and the
       boxes are 4 px apart; the bar's own side padding is 8 px. WORST CASE AT 320 PX, in Inter, is Week beside
       Agenda — the widest word of each button — and it leaves about 11 px; at 375 px it leaves about 66.
       (Measured in Chromium, PhoneShellCaptureTests.ModeSwitchOnTheBar: one row, 44 px each way, no two boxes
       overlapping, nothing past the bar's padding, no sideways scroll, at 375 and 320 on both calendars in both
       modes — with the shipped face loaded and no scrollbar, as a phone has none.) */
    .cal-controls {
        position: fixed;
        z-index: 38;
        left: 0;
        right: 0;
        bottom: 0;
        box-sizing: border-box;
        display: flex;
        align-items: center;
        gap: var(--s1);
        min-height: calc(var(--cal-bar) + env(safe-area-inset-bottom, 0px));
        padding: 0 var(--s2) env(safe-area-inset-bottom, 0px);
        background: var(--surface);
        border-top: 1px solid var(--border-strong);
    }

    .cal-controls .cal-nav {
        order: 1;
        flex: 1 1 auto;
        flex-wrap: nowrap;
        gap: var(--s1);
        min-width: 0;
        margin: 0;
    }

    .cal-controls .cal-nav a {
        position: relative;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        box-sizing: border-box;
        flex: 0 0 auto;
        min-width: 44px;
        min-height: 44px;
        padding: 0 5px;
        font-size: var(--t-small);
        white-space: nowrap;
    }

    .cal-controls .cal-nav a:hover,
    .cal-controls .cal-nav a:focus-visible {
        z-index: 1;
    }

    .cal-controls .cal-nav a.cal-step {
        padding: 0;
        font-size: 18px;
    }

    /* ‹ Today › joined: each box after the first is drawn over its neighbour's border, so the three share one. */
    .cal-controls .cal-nav a[rel="prev"] {
        border-start-end-radius: 0;
        border-end-end-radius: 0;
    }

    .cal-controls .cal-nav a.cal-today-link {
        margin-inline-start: calc(-1px - var(--s1));
        border-radius: 0;
    }

    .cal-controls .cal-nav a[rel="next"] {
        margin-inline-start: calc(-1px - var(--s1));
        border-start-start-radius: 0;
        border-end-start-radius: 0;
    }

    /* The date is in the grid's own heads, and By clinician / By room are the sheet's: a time axis has no meaning in
       the agenda and the bar has no room for it in the grid. */
    .cal-controls .cal-nav .cal-showing,
    .cal-controls .cal-nav .cal-axes {
        display: none;
    }

    .cal-controls .cal-nav .cal-spans {
        margin-inline-start: auto;
    }

    /* A PAIR IS ONE BUTTON HERE (SAAS-521): the link marked current is not drawn, the other one is, so the button
       names where it goes. The wide screen's divider and padding are not wanted on a phone. */
    .cal-controls .cal-nav .cal-spans,
    .cal-controls .cal-nav .cal-views {
        padding: 0;
        border: 0;
    }

    .cal-controls .cal-nav .cal-spans a.current,
    .cal-controls .cal-nav .cal-views a.current {
        display: none;
    }

    .cal-bar-more {
        order: 2;
    }

    .cal-bar-more > summary {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        box-sizing: border-box;
        width: 44px;
        min-height: 44px;
        border: 1px solid var(--border-strong);
        border-radius: var(--r-sm);
        background: var(--surface);
        color: var(--ink);
    }

    .cal-bar-more[open] > summary {
        border-color: var(--ink-strong);
        color: var(--ink-strong);
    }

    /* THE SHEET: above the bar, the width of the screen, scrolling inside itself. */
    .cal-bar-sheet {
        position: absolute;
        left: 0;
        right: 0;
        bottom: 100%;
        box-sizing: border-box;
        display: flex;
        flex-direction: column;
        align-items: stretch;
        gap: var(--s3);
        max-height: calc(100vh - var(--bar) - var(--cal-bar) - var(--s4));
        max-height: calc(100dvh - var(--bar) - var(--cal-bar) - var(--s4));
        overflow-y: auto;
        padding: var(--s3) var(--s4) var(--s4);
        background: var(--surface);
        border-top: 1px solid var(--border-strong);
        border-radius: var(--r-lg) var(--r-lg) 0 0;
        box-shadow: var(--e2);
        color: var(--ink);
    }

    .cal-bar-sheet > * {
        margin: 0;
    }

    .cal-bar-sheet .cal-phone-only {
        display: flex;
    }

    /* The axis switch's second copy is last in the markup and first in the sheet. (Grid / Agenda has none: it is
       on the bar, SAAS-521.) */
    .cal-bar-sheet .cal-axes.cal-phone-only {
        order: -1;
    }

    /* The step links' words: "‹ Previous", "Next ›" at a wide width; the arrow alone in the bar, with
       the word kept for a screen reader. */
    .cal-controls .cal-step-word {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
    }

    .cal-bar-sheet .cal-axes {
        padding: 0;
        border: 0;
    }

    .cal-bar-sheet .cal-axes a {
        flex: 1 1 0;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-height: 44px;
        padding: var(--s1) var(--s3);
        border: 1px solid var(--border-strong);
        border-radius: var(--r-sm);
        background: var(--surface);
        color: var(--ink);
        text-decoration: none;
    }

    .cal-bar-sheet .cal-axes a.current {
        border-color: var(--ink-strong);
        color: var(--ink-strong);
        font-weight: 650;
    }

    .cal-bar-sheet .cal-bar-about {
        display: block;
        color: var(--muted);
        font-size: var(--t-small);
    }

    .cal-bar-sheet .cal-bar-about p {
        margin: 0;
    }

    /* THE GRID, BETWEEN THE TWO BARS. It is its own scroller — a sticky head needs a scroller whose top
       is the head's, and the page is not one the grid can stick inside — filling the screen from the
       bar down to the bottom bar, edge to edge. The day and the column heads stay under the top bar
       while its rows scroll. The strip is 41px (a 20px date, a 20px column line, a 1px rule), so the
       first hour row sits at 56 + 41: under 100px from the top of the screen. */
    .portal-calendar .calendar {
        max-height: calc(100vh - var(--bar) - var(--cal-bar) - env(safe-area-inset-bottom, 0px));
        max-height: calc(100dvh - var(--bar) - var(--cal-bar) - env(safe-area-inset-bottom, 0px));
        margin-inline: calc(var(--s4) * -1);
        border-width: 0 0 1px;
        border-radius: 0;
        box-shadow: none;
    }

    /* THE NARROWING'S LINE, ABOVE THE GRID (SAAS-491): state, in the ink a stated fact takes. It is page
       content above the grid, like the Copy synced line and the clinic key's line, and the grid keeps its
       height: the page scrolls until the grid fills the space between the bars. */
    .portal-calendar > .cal-narrowed-line {
        display: block;
        margin: 0 0 var(--s2);
        font-size: var(--t-small);
        font-weight: 600;
        color: var(--ink-strong);
    }

    .portal-calendar .cal-corner,
    .portal-calendar .mw-corner {
        position: sticky;
        top: 0;
        z-index: 3;
        height: 41px;
        background: var(--surface);
        border-bottom: 1px solid var(--border-strong);
        box-sizing: border-box;
    }

    .portal-calendar .cal-date,
    .portal-calendar .mw-date {
        position: sticky;
        top: 0;
        z-index: 2;
        box-sizing: border-box;
        height: 20px;
        padding: 0 var(--s2);
        line-height: 20px;
        background: var(--surface);
    }

    .portal-calendar .cal-lane-heads,
    .portal-calendar .mw-count {
        position: sticky;
        top: 20px;
        z-index: 2;
        box-sizing: border-box;
        height: 21px;
        background: var(--surface);
    }

    .portal-calendar .cal-lane-head {
        padding: 0 var(--s1);
        line-height: 20px;
    }

    .portal-calendar .mw-count {
        padding: 0 var(--s2);
        line-height: 20px;
    }

    /* THE AGENDA HAS NO COLUMNS AND SO NO STRIP OF HEADS, and its day lines do not stick. They were
       made to (a `top: var(--bar)` on `.ma-date`) and the first capture showed why not: `.ma` clips
       its rounded corners with `overflow: hidden`, which makes it the scroll container a sticky
       child measures against, so the "stuck" line sat 56px down inside its own card. The agenda
       scrolls with the page under the top bar, and the bar's title is what names the page. */
}

/* =============================================================================================
   THE ADVANCED FILTER, AND EVERY LIST PAGE OPENING ON ITS DATA (SAAS-473, ADR 0020 addendum of
   2026-10-03)

   Darius, 2026-10-03: "hide the filter into a collapsible panel that said 'advance filter' so we have
   more room, the quick date filter should be outside this collapsible panel ... data should be
   already viewed in the first page everywhere."

   ONE ROW, OUTSIDE THE PANEL: the quick dates, the date range (on a desktop), and the page's one
   primary search with its own submit. UNDER IT: the panel's summary, `Advanced filter · N`, with a
   chip per set filter and Clear filters beside it. The panel opens under that row and pushes the
   list down — nothing floats over the rows a reader came for.

   The filter form stops being a card: it was a bordered, padded box around eight controls, and the
   controls are now one row. Tokens only (ADR 0020); no colour of its own.
   ============================================================================================= */

.branch-form.filter-form,
.register-form.filter-form {
    display: block;
    margin: 0 0 var(--s4);
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
}

.filter-strip {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--s2) var(--s4);
}

.filter-strip > .quick-ranges {
    margin: 0;
    min-width: 0;
}

/* The phone's way to the range; wide, the range stands in this row, so the item has nothing to do. */
.quick-ranges li.dates-more {
    display: none;
}

/* Drawn as a quick range, and not one: it opens the panel at the range rather than naming a range. */
.quick-ranges a.dates-link {
    display: inline-block;
    padding: calc(var(--s1) / 2) var(--s2);
    border: 1px dashed var(--border-strong);
    border-radius: var(--r-sm);
    font-size: var(--t-small);
    text-decoration: none;
    color: var(--ink);
    white-space: nowrap;
}

.filter-strip .field {
    display: flex;
    flex-direction: column;
    gap: var(--s1);
    min-width: 0;
}

/* A label and the mark that explains it, on one line (SAAS-462's rule for a heading, for a field). */
.filter-strip .field-head {
    display: flex;
    align-items: center;
    gap: 0;
    min-height: 28px;
}

.filter-strip .field-head label,
.filter-strip .field > label {
    font-size: var(--t-small);
    font-weight: 600;
    letter-spacing: .01em;
    color: var(--muted);
}

.filter-strip input[type="search"],
.filter-strip input[type="date"] {
    box-sizing: border-box;
    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;
}

.filter-strip .range {
    display: flex;
    align-items: center;
    gap: var(--s2);
}

.filter-strip .range input {
    flex: 1 1 0;
    width: 10rem;
}

.filter-strip .range-to {
    flex: 0 0 auto;
    font-size: var(--t-small);
    color: var(--muted);
}

/* THE PRIMARY SEARCH: the box and its own submit, side by side. */
.filter-strip .search-row {
    display: flex;
    align-items: stretch;
    gap: var(--s2);
}

.filter-strip .primary-search input[type="search"] {
    width: 15rem;
}

/* An information mark inside the filter form is the mark, not one of the form's outlined buttons. */
.filter-form .info-mark .info-mark-button,
.filter-form .info-mark .info-mark-button:hover {
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
}

/* The dates, dimmed while a receipt number reads every date (SAAS-472) — the range and the quick dates
   both, now that the quick dates stand in the row rather than inside the range. */
.filter-strip.dates-idle > .quick-ranges,
.filter-strip .field-range.dates-idle .range {
    opacity: 0.55;
}

.filter-strip p.stated.dates-not-applied {
    margin: 0;
    align-self: center;
    padding-left: var(--s2);
    border-left: 3px solid var(--warn);
    color: var(--ink);
    font-weight: 500;
}

/* --- The panel's summary: a button-weight control, its count, and its chevron --------------------- */

.advanced-filter > summary {
    list-style: none;
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    box-sizing: border-box;
    min-height: 36px;
    padding: 0 var(--s3);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    font-size: var(--t-body);
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
}

.advanced-filter > summary::-webkit-details-marker {
    display: none;
}

/* The chevron, drawn by the border trick so it needs no glyph from the typeface. */
.advanced-filter > summary::after {
    content: "";
    width: 6px;
    height: 6px;
    margin-left: var(--s1);
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: translateY(-2px) rotate(45deg);
}

.advanced-filter[open] > summary::after {
    transform: translateY(2px) rotate(-135deg);
}

.advanced-filter > summary:hover {
    background: var(--row-hover);
}

.advanced-filter-count {
    font-weight: 650;
    color: var(--ink-strong);
}

/* The panel: the fields, one bordered box under the row. */
.advanced-filter-fields {
    box-sizing: border-box;
    padding: var(--s4);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    background: var(--surface);
    box-shadow: var(--e1);
}

/* The filters inside it keep SAAS-72's grid; the rule above them was the card's own edge. */
.advanced-filter-fields .ledger-filters {
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
}

.advanced-filter-fields .actions {
    display: flex;
    align-items: center;
    gap: var(--s4);
    margin-top: var(--s4);
}

/* The register's filters keep SAAS-69's grid inside the panel — the form itself is no longer it. */
.advanced-filter-fields .register-fields {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    align-items: end;
    gap: var(--s3) var(--s4);
}

/* --- The chips: what is set, and the way to drop it --------------------------------------------- */

.filter-chips {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2) var(--s3);
    min-width: 0;
}

.filter-chips ul {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    margin: 0;
    padding: 0;
    list-style: none;
}

.filter-chips a.filter-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    box-sizing: border-box;
    min-height: 32px;
    padding: 0 var(--s2) 0 var(--s3);
    border: 1px solid color-mix(in srgb, var(--ink) 25%, transparent);
    border-radius: 999px;
    background: color-mix(in srgb, var(--ink) 6%, transparent);
    color: var(--ink-strong);
    font-size: var(--t-small);
    font-weight: 500;
    text-decoration: none;
    white-space: nowrap;
}

.filter-chips a.filter-chip:hover {
    background: var(--row-hover);
}

.filter-chip-x {
    font-size: var(--t-body);
    line-height: 1;
    color: var(--muted);
}

.filter-chips a.clear-filters {
    display: inline-flex;
    align-items: center;
    font-size: var(--t-small);
    color: var(--muted);
    white-space: nowrap;
}

.filter-chips a.clear-filters:hover {
    color: var(--ink);
}

/* The export link and its mark, on one line. */
.portal-billing .export {
    align-items: center;
    margin: 0 0 var(--s3);
}

/* --- THE DISCLOSURE, MADE TRANSPARENT TO LAYOUT -------------------------------------------------
   SAAS-463's technique (`display: contents` with `::details-content`), behind the same feature
   query and for the same reason: a browser without `::details-content` keeps an ordinary
   disclosure — summary, then the panel with the range inside it — and loses nothing.

   With it, the <details> and its content slot draw no box, so the summary, the chips and the panel
   are items of the strip and can be ordered: the row first, then a line break, then the summary
   with the chips beside it, then the panel. What the browser's own closed state used to hide is
   now hidden by the `open` attribute here, so the page's markup and the server's `open` mean
   exactly what they did. */
@supports selector(::details-content) {
    .filter-strip .advanced-filter {
        display: contents;
    }

    .filter-strip .advanced-filter::details-content {
        display: contents;
        content-visibility: visible;
    }

    .filter-strip > .quick-ranges {
        order: 1;
    }

    .filter-strip > .dates-not-applied {
        order: 3;
    }

    .filter-strip > .primary-search {
        order: 4;
    }

    /* The line break between the row and the summary. */
    .filter-strip::after {
        content: "";
        order: 5;
        flex-basis: 100%;
        height: 0;
    }

    .filter-strip .advanced-filter > summary {
        order: 6;
    }

    .filter-strip > .filter-chips {
        order: 7;
    }

    .filter-strip .advanced-filter-panel {
        order: 8;
        flex-basis: 100%;
    }

    .filter-strip .advanced-filter:not([open]) .advanced-filter-fields {
        display: none;
    }
}

/* WIDE: the range stands in the row, beside the quick dates, whether the panel is open or not; the
   rest of the panel follows `open`. */
@media (min-width: 768px) {
    @supports selector(::details-content) {
        .filter-strip .advanced-filter-panel {
            display: contents;
        }

        .filter-strip .advanced-filter-panel > .field-range {
            order: 2;
        }

        .filter-strip .advanced-filter-fields {
            order: 8;
            flex-basis: 100%;
        }
    }
}

/* PHONE: the views as a segmented row, the quick dates as one row that scrolls sideways, the search
   the width of the page, and the range inside the panel — `Dates…` opens it there. */
@media screen and (max-width: 767.98px) {
    .filter-strip {
        gap: var(--s2) var(--s2);
    }

    .filter-strip > .quick-ranges {
        flex: 1 1 100%;
        max-width: 100%;
        overflow-x: auto;
        scrollbar-width: none;
    }

    .filter-strip > .quick-ranges ul {
        flex-wrap: nowrap;
    }

    .filter-strip > .quick-ranges a.quick {
        white-space: nowrap;
    }

    .quick-ranges li.dates-more {
        display: list-item;
    }

    .filter-strip > .primary-search {
        flex: 1 1 100%;
    }

    .filter-strip .primary-search input[type="search"] {
        flex: 1 1 auto;
        width: auto;
    }

    .filter-strip > .dates-not-applied {
        flex: 1 1 100%;
    }

    .filter-strip .advanced-filter-panel {
        box-sizing: border-box;
        padding: var(--s3);
        border: 1px solid var(--border);
        border-radius: var(--r-lg);
        background: var(--surface);
    }

    .filter-strip .advanced-filter-panel .advanced-filter-fields {
        padding: 0;
        border: 0;
        box-shadow: none;
        background: none;
    }

    .filter-strip .advanced-filter-panel > .field-range {
        margin-bottom: var(--s3);
    }

    /* Two date boxes share the panel's width rather than each asking for 10rem of a 343px page. */
    .filter-strip .range input {
        width: 0;
        flex: 1 1 0;
    }

    @supports selector(::details-content) {
        .filter-strip .advanced-filter:not([open]) .advanced-filter-panel {
            display: none;
        }
    }

    /* The Surface's views, the second copy (ShellViews `OnThePhone`): one segmented row. */
    .phone-views {
        margin: 0 0 var(--s3);
    }

    .phone-views .surface-views ul {
        display: flex;
        gap: 2px;
        padding: 2px;
        border: 1px solid var(--border);
        border-radius: var(--r-md);
        background: var(--surface);
    }

    .phone-views .surface-views li {
        flex: 1 1 0;
        min-width: 0;
    }

    .phone-views .surface-views a.view {
        justify-content: center;
        min-height: 40px;
        padding: 0 var(--s2);
        white-space: nowrap;
    }
}

/* Wide, and paper: the phone's copy of the views is not drawn — the rail holds them. */
@media (min-width: 768px), print {
    .phone-views {
        display: none;
    }
}

/* =============================================================================================
   THE LIST ON A PHONE, AS CARDS (SAAS-473 ruling 5)

   Below 768 px a ledger of eleven columns and a diary of nine are a horizontal scroll a thumb has
   to fight before it meets a single row. So each row is drawn as a short card, in the drawing's
   order — line 1: the number (or the time) and the money; line 2: the date (or the time), the
   patient and the state — with CSS alone, on the SAME <table> markup: the cells keep their
   columns' headers in the document (the <thead> is visually hidden, not removed), and every row
   still links where it linked. A desktop draws the table untouched: none of this applies at
   768 px and up, and the table-rectangle comparison at 1440 holds it to that.

   WHAT THE CARD DROPS, PER TABLE — reachable on the row's own page (the receipt, the patient):
     receipts  Raised by, Performed by, Billed to, Collected, Balance, Remark (the receipt shows them)
     payments  Taken by, Remark (the receipt shows the bill; the clinic is the small line)
     deposits  Taken by, Remark (the receipt number and the clinic are the small line)
     diary     Category (the clinic, clinician and type are the small line)
     book      Category (the clinician, type and room are the small line)
     register  Billing scheme (the registration date is the small line; the patient's page holds all)
   The card never widens the page: every cell may wrap or truncate inside the card's own width.
   ============================================================================================= */

@media screen and (max-width: 767.98px) {
    table.ledger[data-cards],
    table.register[data-cards] {
        display: block;
        width: 100%;
        min-width: 0;
    }

    table.ledger[data-cards] > thead {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    table.ledger[data-cards] > tbody,
    table.register[data-cards] > tbody {
        display: block;
    }

    table.ledger[data-cards] > tbody > tr,
    table.register[data-cards] > tbody > tr {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr) auto;
        column-gap: var(--s2);
        row-gap: 2px;
        align-items: baseline;
        padding: var(--s2) var(--s1);
        border-bottom: 1px solid var(--line);
    }

    /* Every cell is hidden unless a card below places it. */
    table.ledger[data-cards] > tbody > tr > td,
    table.register[data-cards] > tbody > tr > td {
        display: none;
        min-width: 0;
        padding: 0;
        border: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    table.ledger[data-cards] > tbody > tr > td.receipt-no,
    table.ledger[data-cards] > tbody > tr > td.patient-name,
    table.ledger[data-cards] > tbody > tr > td.state,
    table.register[data-cards] > tbody > tr > td.patient-name {
        display: block;
    }

    /* The line 1 lead — a number, a time, a name — is the row's handle and reads at body weight. */
    table.ledger[data-cards] > tbody > tr > td.receipt-no,
    table.register[data-cards] > tbody > tr > td.patient-name {
        grid-column: 1 / 3;
        grid-row: 1;
        font-weight: 600;
    }

    table.ledger[data-cards] > tbody > tr > td.state {
        grid-column: 3;
        grid-row: 2;
        text-align: right;
        font-size: var(--t-small);
    }

    table.ledger[data-cards] > tbody > tr > td.patient-name {
        grid-column: 2;
        grid-row: 2;
        font-size: var(--t-small);
    }

    /* --- receipts: no. | total ; issued · patient | state ; clinic ------------------------------- */
    table.ledger[data-cards="receipts"] > tbody > tr > td[data-col="total"],
    table.ledger[data-cards="payments"] > tbody > tr > td[data-col="amount"],
    table.ledger[data-cards="deposits"] > tbody > tr > td[data-col="amount"] {
        display: block;
        grid-column: 3;
        grid-row: 1;
        text-align: right;
        font-weight: 600;
    }

    table.ledger[data-cards="receipts"] > tbody > tr > td[data-col="issued"],
    table.ledger[data-cards="payments"] > tbody > tr > td[data-col="taken"],
    table.ledger[data-cards="deposits"] > tbody > tr > td[data-col="moved"] {
        display: block;
        grid-column: 1;
        grid-row: 2;
        font-size: var(--t-small);
        color: var(--muted);
    }

    table.ledger[data-cards="receipts"] > tbody > tr > td.clinic,
    table.ledger[data-cards="payments"] > tbody > tr > td.clinic,
    table.ledger[data-cards="deposits"] > tbody > tr > td.clinic {
        display: block;
        grid-column: 1 / -1;
        grid-row: 3;
        font-size: var(--t-micro);
        color: var(--muted);
    }

    /* A grouped receipts list keeps its clinic heading as the group's own line. */
    table.ledger[data-cards="receipts"] > tbody > tr.group-head {
        display: block;
        padding: var(--s3) var(--s1) var(--s1);
    }

    table.ledger[data-cards="receipts"] > tbody > tr.group-head > td {
        display: none;
    }

    table.ledger[data-cards="receipts"] > tbody > tr.group-head > th {
        display: block;
        padding: 0;
        border: 0;
    }

    /* --- payments: no. | amount ; taken · patient | method ; clinic ------------------------------ */
    table.ledger[data-cards="payments"] > tbody > tr > td.method,
    table.ledger[data-cards="deposits"] > tbody > tr > td.method {
        display: block;
        grid-column: 3;
        grid-row: 2;
        text-align: right;
        font-size: var(--t-small);
    }

    /* --- deposits: movement | amount ; moved · patient | method ; clinic · receipt no. ----------- */
    table.ledger[data-cards="deposits"] > tbody > tr > td.movement {
        display: block;
        grid-column: 1 / 3;
        grid-row: 1;
        font-weight: 600;
    }

    table.ledger[data-cards="deposits"] > tbody > tr > td.receipt-no {
        grid-column: 3;
        grid-row: 3;
        text-align: right;
        font-size: var(--t-micro);
        font-weight: 400;
        color: var(--muted);
    }

    table.ledger[data-cards="deposits"] > tbody > tr > td.clinic {
        grid-column: 1 / 3;
    }

    /* --- diary and book: when | for ; patient | status ; clinic · clinician · type (· room) ------- */
    table.ledger[data-cards="diary"] > tbody > tr > td[data-col="when"],
    table.ledger[data-cards="book"] > tbody > tr > td[data-col="when"] {
        display: block;
        grid-column: 1 / 3;
        grid-row: 1;
        font-weight: 600;
    }

    table.ledger[data-cards="diary"] > tbody > tr > td[data-col="for"],
    table.ledger[data-cards="book"] > tbody > tr > td[data-col="for"] {
        display: block;
        grid-column: 3;
        grid-row: 1;
        text-align: right;
        color: var(--muted);
    }

    table.ledger[data-cards="diary"] > tbody > tr > td.patient-name,
    table.ledger[data-cards="book"] > tbody > tr > td.patient-name {
        grid-column: 1 / 3;
        font-size: var(--t-body);
    }

    table.ledger[data-cards="diary"] > tbody > tr > td.clinic,
    table.ledger[data-cards="diary"] > tbody > tr > td[data-col="resource"],
    table.ledger[data-cards="diary"] > tbody > tr > td[data-col="type"],
    table.ledger[data-cards="book"] > tbody > tr > td[data-col="resource"],
    table.ledger[data-cards="book"] > tbody > tr > td[data-col="type"],
    table.ledger[data-cards="book"] > tbody > tr > td.room {
        display: block;
        grid-row: 3;
        font-size: var(--t-micro);
        color: var(--muted);
    }

    table.ledger[data-cards="diary"] > tbody > tr > td.clinic,
    table.ledger[data-cards="book"] > tbody > tr > td[data-col="resource"] {
        grid-column: 1;
    }

    table.ledger[data-cards="diary"] > tbody > tr > td[data-col="resource"],
    table.ledger[data-cards="book"] > tbody > tr > td[data-col="type"] {
        grid-column: 2;
    }

    table.ledger[data-cards="diary"] > tbody > tr > td[data-col="type"],
    table.ledger[data-cards="book"] > tbody > tr > td.room {
        grid-column: 3;
        text-align: right;
    }

    /* --- register: name | no. ; born · phone | registered ------------------------------------------ */

    /* THE REGISTER'S HEADINGS ARE ITS SORT, so they stay: one wrapping row of the same links above the
       cards, in place of a header row nobody could line up with a card. */
    table.register[data-cards] > thead,
    table.register[data-cards] > thead > tr {
        display: flex;
        flex-wrap: wrap;
        gap: 0 var(--s3);
    }

    table.register[data-cards] > thead > tr > th {
        position: static;
        display: block;
        padding: 0;
        border: 0;
        box-shadow: none;
        background: none;
    }

    /* A register read for several clinics (SAAS-512): the row of sort links is a row of links, and the Branch heading is not
       one -- the card names its clinic on its own last line, so the heading is left off the phone's row. */
    table.register[data-cards] > thead > tr > th.clinic {
        display: none;
    }

    table.register[data-cards="register"] > tbody > tr > td[data-col="number"] {
        display: block;
        grid-column: 3;
        grid-row: 1;
        text-align: right;
        color: var(--muted);
    }

    table.register[data-cards="register"] > tbody > tr > td[data-col="birthday"] {
        display: block;
        grid-column: 1;
        grid-row: 2;
        font-size: var(--t-small);
        color: var(--muted);
    }

    table.register[data-cards="register"] > tbody > tr > td.phone {
        display: block;
        grid-column: 2;
        grid-row: 2;
        font-size: var(--t-small);
    }

    table.register[data-cards="register"] > tbody > tr > td[data-col="registered"] {
        display: block;
        grid-column: 3;
        grid-row: 2;
        text-align: right;
        font-size: var(--t-small);
        color: var(--muted);
    }

    /* A register read for several clinics (SAAS-512): the clinic is the card's third line, small and muted, the way the
       Billing cards carry theirs -- under the name and the number, where it names whose patient the card is. */
    table.register[data-cards="register"] > tbody > tr > td.clinic {
        display: block;
        grid-column: 1 / -1;
        grid-row: 3;
        font-size: var(--t-micro);
        color: var(--muted);
    }

    /* The sorted column's tint marks a column; a card has none, so the tint would mark one word. */
    table.ledger[data-cards] > tbody > tr > td.sorted,
    table.register[data-cards] > tbody > tr > td.sorted {
        background: none;
    }

    /* The scroll boxes have nothing left to scroll sideways; the page never widens either way. */
    .ledger-scroll,
    .register-scroll {
        overflow-x: hidden;
    }
}

/* =============================================================================================
   THE PHONE PASS ON THE LIST PAGES (SAAS-473, the coordinator's review of part a)

   At 375 px the first receipt still stood at 741 px: the opening-range lead, a label row over the
   receipt number, the summary, the chips and Clear filters on three rows, the export line, the
   narrowed line and a sort control wrapping over three rows. Darius's aim is "data should be
   already viewed in the first page everywhere", and the drawing's first card is about 250 px down.
   Every rule below is inside `max-width: 767.98px` (or hides a phone-only copy wide), so 768 px and
   up is unchanged — measured, not assumed.
   ============================================================================================= */

/* The magnifier is the phone's Find; wide, the button keeps its word. */
.filter-strip button.find .find-icon {
    display: none;
}

/* The export's phone copy, at the panel's foot, is not drawn wide — the page's own copy is. */
@media (min-width: 768px), print {
    .advanced-filter-foot {
        display: none;
    }
}

@media screen and (max-width: 767.98px) {
    /* 1. THE OPENING-RANGE LEAD IS NOT DRAWN: the current quick date states the range, and the
       sentence is in the title's mark, one tap away. Whose bookings these are (Appointments' doctor
       lead) stays: only its range half wears the class. */
    .portal-billing .opening-range {
        display: none;
    }

    /* 2. ONE ROW: [Receipt number][Find][ⓘ][Filter · N]. The strip's line break before the summary
       goes, so the summary follows whatever stands before it: the search where there is one, else
       the quick dates, which then shrink and scroll so the summary ends their row. */
    .portal-billing .filter-strip::after {
        display: none;
    }

    .portal-billing .filter-strip > .quick-ranges {
        flex: 1 1 0;
        min-width: 0;
    }

    .portal-billing .filter-strip:has(> .primary-search) > .quick-ranges {
        flex: 1 1 100%;
    }

    .portal-billing .filter-strip > .primary-search {
        flex: 1 1 0;
        flex-direction: row;
        align-items: center;
        gap: var(--s1);
        min-width: 0;
    }

    /* The label stays the box's label, in the document, and is not drawn: the placeholder says it. */
    .portal-billing .filter-strip > .primary-search > .field-head {
        display: contents;
    }

    .portal-billing .filter-strip > .primary-search label {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .portal-billing .filter-strip > .primary-search .search-row {
        order: 1;
        flex: 1 1 auto;
        min-width: 0;
        gap: var(--s1);
    }

    .portal-billing .filter-strip > .primary-search .info-mark {
        order: 2;
        flex: none;
    }

    .portal-billing .filter-strip > .primary-search input[type="search"] {
        flex: 1 1 auto;
        width: 0;
        min-width: 0;
    }

    .portal-billing .filter-strip button.find {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 40px;
        padding: 0 var(--s2);
    }

    .portal-billing .filter-strip button.find .find-icon {
        display: block;
    }

    .portal-billing .filter-strip button.find .find-word {
        display: none;
    }

    /* The summary reads "Filter · N": "Advanced " stays in its name, visually hidden. */
    .portal-billing .advanced-filter > summary {
        flex: none;
        padding: 0 var(--s2);
    }

    .portal-billing .advanced-filter-adjective {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .portal-billing .advanced-filter-noun {
        display: inline-block;
    }

    .portal-billing .advanced-filter-noun::first-letter {
        text-transform: uppercase;
    }

    /* 3. THE CHIPS AND CLEAR FILTERS, ONE WRAPPING ROW: inline flow, so Clear filters follows the
       last chip on its line. */
    .portal-billing .filter-strip > .filter-chips,
    .portal-analytics .filter-strip > .filter-chips {
        display: block;
        flex: 1 1 100%;
        line-height: 1.9;
    }

    .portal-billing .filter-chips ul,
    .portal-analytics .filter-chips ul {
        display: inline;
    }

    .portal-billing .filter-chips li,
    .portal-analytics .filter-chips li {
        display: inline-block;
        margin: 0 var(--s1) var(--s1) 0;
        vertical-align: middle;
    }

    .portal-billing .filter-chips a.filter-chip,
    .portal-analytics .filter-chips a.filter-chip {
        min-height: 26px;
        line-height: 1.2;
    }

    .portal-billing .filter-chips a.clear-filters,
    .portal-analytics .filter-chips a.clear-filters {
        vertical-align: middle;
    }

    /* 4. THE SORT: one row of small links that scrolls sideways in its own box, the quick dates'
       treatment, with its label on the same line. */
    .portal-billing .sort-control {
        flex-wrap: nowrap;
        align-items: center;
        gap: var(--s2);
        margin: 0 0 var(--s2);
        overflow-x: auto;
        scrollbar-width: none;
    }

    .portal-billing .sort-control .sort-label {
        flex: none;
        white-space: nowrap;
    }

    .portal-billing .sort-control ul {
        flex-wrap: nowrap;
        align-items: center;
    }

    .portal-billing .sort-control a.sort {
        padding: 2px var(--s2);
        font-size: var(--t-small);
        white-space: nowrap;
    }

    /* 5. THE EXPORT IS READ AT THE PANEL'S FOOT. */
    .portal-billing .export-wide {
        display: none;
    }

    .advanced-filter-foot {
        margin-top: var(--s3);
    }

    .advanced-filter-foot .export {
        margin: 0;
    }

    /* 6. THE NARROWED LINE STAYS — it is state — in the page's small type. */
    .portal-billing .narrowed {
        margin: 0 0 var(--s2);
        font-size: var(--t-small);
        line-height: 1.35;
    }

    /* 7. THE SPACING BETWEEN THEM, tightened to the phone's own scale: the views row, the synced line
       and the form each give back half their gap, and a block's heading sits closer to its rows. */
    .portal-billing .phone-views {
        margin-bottom: var(--s2);
    }

    .portal-billing > p.copy-synced {
        margin-bottom: var(--s2);
    }

    .portal-billing .branch-form.filter-form {
        margin-bottom: var(--s2);
    }

    .portal-billing .branch-block h2 {
        margin-bottom: var(--s1);
    }

    /* A block's "Open the calendar for this branch" moves to the block's foot, under its pager: it
       is a way onward from the rows, and above them it stood between the clinic's name and its first
       booking. Same link, same address; only its place in the column changes, and only on a phone. */
    .portal-billing .branch-block {
        display: flex;
        flex-direction: column;
    }

    .portal-billing .branch-block .block-link {
        order: 1;
        margin: var(--s2) 0 0;
    }
}

/* =============================================================================================
   PART B: THE DASHBOARD OPENS ON ITS FIGURES (SAAS-473 part b, ruling 2)

   The period's row stays: Month | Year and the stepper, whose label is the jump list (SAAS-353).
   The patient comparison and the clinic picker are inside the Advanced filter, closed when the
   page opens, with a chip beside the summary for a selection and for the payer basis. Below
   768 px the standfirst is not drawn (its sentence is in the title's mark) and the export is read
   at the panel's foot, Billing's rules from part a.
   ============================================================================================= */
.portal-analytics .dashboard-filters {
    margin: 0 0 var(--s4);
}

.portal-analytics .dashboard-basis {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2) var(--s3);
    margin: 0 0 var(--s3);
}

.portal-analytics .dashboard-basis-label {
    font-size: var(--t-small);
    font-weight: 600;
    color: var(--muted);
}

.portal-analytics .dashboard-filters .branch-chips {
    margin: 0;
}

.portal-analytics .dashboard-export {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    margin: 0 0 var(--s5);
}

.portal-analytics .dashboard-export p.export {
    margin: 0;
}

.advanced-filter-foot .dashboard-export {
    margin: 0;
}

@media screen and (max-width: 767.98px) {
    .portal-analytics .dashboard-standfirst,
    .portal-analytics .export-wide {
        display: none;
    }

    /* "Filter · N" on a phone, as on Billing: "Advanced " stays in the summary's name, unseen. */
    .portal-analytics .advanced-filter-adjective {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .portal-analytics .advanced-filter-noun {
        display: inline-block;
    }

    .portal-analytics .advanced-filter-noun::first-letter {
        text-transform: uppercase;
    }
}
/* The Settings screens (SAAS-479): a Branch's managed app_control rows, and a Tenant's Branches side by side. Code in the cells is a
   value to be read exactly — a trailing space is the whole difference between 0 and "0 " — so it keeps its spaces and wraps. A live
   master switch is the one stated failure on these screens, and wears --bad for that reason; a settled row steps back. */
.settings-table td code {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.settings-table td .settings-live {
    color: var(--bad);
    font-weight: 600;
}

.settings-table td .settings-settled {
    color: var(--muted);
}

.operator-tenant-settings #cleared-means-all dd {
    margin: 0 0 var(--s2);
    max-width: 80ch;
}

/* A row's name is an app_control key and is read exactly: it never breaks across lines, and the header's capitals must not turn it
   into a different key — `def_saas_url` and `DEF_SAAS_URL` are two names to a person reading them off the screen. */
.settings-table td:first-child code {
    white-space: nowrap;
}

.settings-table th code {
    text-transform: none;
}

/* The decision forms. One field to a line, its hint beneath it, the radio beside its own words: the clear option carries a long
   sentence about what clearing means for THIS row, and set inline beside the box it would run into the next option. */
.operator-branch-settings .field,
.operator-tenant-settings .field {
    margin: 0 0 var(--s3);
}

.operator-branch-settings .field .hint,
.operator-tenant-settings .field .hint {
    display: block;
    max-width: 80ch;
    margin-top: var(--s1);
}

.operator-branch-settings .field label,
.operator-tenant-settings .field label {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
}

/* The three suggestions on the Tenant screen, each its own link with room beside it. */
.settings-suggestions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2) var(--s4);
    align-items: baseline;
}
