@import '_content/KTS.Foundation/KTS.Foundation.wwzm5ic6oq.bundle.scp.css';

/* /Components/AppointmentLegend.razor.rz.scp.css */
/* The footer status legend as a filter (#1109).

   Scoped to this component on purpose. Pages/Calendar.razor.css cannot reach in
   here — Blazor stamps a different scope attribute on a child component's
   elements, so a rule written there would need ::deep and would then leak into
   whatever else the page nests. The legend owns its own states.

   Everything the row already had (the flex box, the 0.875rem swatch, the 0.75rem
   label) is still Tailwind, unchanged, so this file adds only what a BUTTON needs
   that a div did not: the reset, the two states and the focus ring. Lengths are
   rem for the same reason Calendar.razor.css states them in rem (#1018, WCAG
   1.4.4). */

.legend-entry[b-6e8p2dtobp] {
    appearance: none;
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}

/* ── THE DIMMED ("filtered out") STATE, MEASURED (#1509) ─────────────────────
   It was `opacity: 0.35`, chosen to match `.doctor-avatar-btn.is-dimmed` on the
   medic medallions "so the two filters on the page look like one mechanism".
   #1509 measured it, because it put this interactive legend on a SECOND page
   and the issue asks for every pair to be computed rather than assumed:

     dimmed label ink vs the page background   1.76:1 light / 2.85:1 dark
     worst dimmed swatch vs the page background 1.75:1 light / 2.23:1 dark

   against WCAG floors of 4.5:1 (text, 1.4.3) and 3:1 (non-text, 1.4.11). A
   dimmed entry is not an "inactive" control that 1.4.3 exempts — it is a live
   button, and pressing it is how a status is added back — so the text floor
   applies in full. At 0.35 the words were not readable.

   `color-mix()` RATHER THAN `opacity`, and that is not a preference: an
   `opacity` on the element composites at paint time and cannot be read out of
   this file, so a contrast test could only ever restate the number written
   here. A `color-mix` against a named background IS the computation, so
   AgendaEdgeToEdgeContrastTests derives the ratio from these declarations
   instead of trusting them.

   80%, which is the smallest round figure that clears BOTH floors in BOTH
   themes — the binding case is the label in light at 79.6%. Measured after:

     dimmed label ink   4.54:1 light / 9.31:1 dark
     worst dimmed swatch 4.32:1 light (noshow) / 6.24:1 dark (cancelled)

   THE COST, stated rather than glossed: the dim is visibly subtler than the
   0.35 it replaces. The pressed/unpressed distinction does not rest on it —
   `aria-pressed` carries the same fact for everyone (WCAG 1.4.1) and is what
   the medallion row established as the non-colour channel.

   THE DIVERGENCE, also stated: `.doctor-avatar-btn.is-dimmed` still uses
   `opacity: 0.35`, so the two filters no longer dim by the same amount. That
   surface has the same defect — a dimmed medallion's initials measure 1.54:1
   light / 1.99:1 dark against their own fill — but it is a Calendar-side fix
   of its own on a control #1509 only MOVED, and making it here would be an
   unmeasured change to a picture-bearing surface. Raised separately.

   The swatch's half of this is not here: its colour arrives as an inline style
   from AppointmentLegend.razor (the status is not knowable from a stylesheet),
   so the same 80% mix is applied there, against the same background token. */
.legend-entry.is-dimmed span[b-6e8p2dtobp] {
    color: color-mix(in srgb, var(--kts-body-color) 80%, var(--kts-body-bg));
}

/* State, not decoration — so only the TWEEN is gated, never the state itself,
   matching how the medallions treat theirs.

   EACH SELECTOR CARRIES THE PROPERTY THAT ACTUALLY CHANGES ON IT, and getting
   that wrong is silent (raised by Copilot on the #1509/#1512 PR):

     · the entry's `color` now changes on the child SPANS, not on the button —
       the swatch has no `color` and must not inherit the label's — and CSS
       transitions are NOT inherited, so a transition on `.legend-entry` animates
       nothing at all;
     · `.legend-clear` still changes its own `opacity`, so it keeps the opacity
       tween it has always had. It briefly carried a `color` transition it never
       uses, which silently dropped its fade. */
@media (prefers-reduced-motion: no-preference) {
    .legend-entry span[b-6e8p2dtobp] {
        transition: color .2s ease;
    }

    .legend-clear[b-6e8p2dtobp] {
        transition: opacity .2s ease;
    }
}

.legend-entry:hover span[b-6e8p2dtobp] {
    color: var(--kts-body-color);
}

/* Foundation's own focus treatment (2px solid var(--kts-primary)), in rem. The
   offset is positive rather than Foundation's inset -2px: these rows sit on the
   page background with no border of their own to draw the ring inside. */
.legend-entry:focus-visible[b-6e8p2dtobp],
.legend-clear:focus-visible[b-6e8p2dtobp] {
    outline: .125rem solid var(--kts-primary);
    outline-offset: .125rem;
    border-radius: .125rem;
}

.legend-clear[b-6e8p2dtobp] {
    appearance: none;
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    font: inherit;
    font-size: .75rem;
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    color: var(--kts-body-color);
    opacity: .8;
    cursor: pointer;
}

.legend-clear:hover[b-6e8p2dtobp] {
    opacity: 1;
}
/* /Components/Booking/BookingChip.razor.rz.scp.css */
/* Layout-first, token-driven (the SlotPicker.razor.css standard): every colour is a Foundation
   --kts-* token, so both themes are correct by construction — no hex, no !important, and no
   dark-mode rules to keep in sync. This is the whole reason the chip could not exist before
   #895 deleted the light-only page stylesheet. */

.booking-chip[b-3uyv2ich9u] {
    display: flex;
    align-items: center;
    gap: .625rem;
    min-width: 0;
    flex: 1 1 16.25rem;
    padding: var(--book-cluster-gap) var(--book-pane-inset);
    border: .0625rem solid var(--kts-border-subtle);
    border-radius: .625rem;
    background: var(--kts-surface);
}

    /* The open state is a border/ink change, never colour alone (WCAG 1.4.1): the chip's action
       also flips its label to "Cancel" and carries aria-expanded, so the state is readable
       without seeing the ring at all. */
    .booking-chip.is-open[b-3uyv2ich9u] {
        border-color: var(--kts-primary);
        background: var(--kts-surface-selected);
    }

.chip-figure[b-3uyv2ich9u] {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    color: var(--kts-text-muted);
}

.chip-body[b-3uyv2ich9u] {
    display: flex;
    flex-direction: column;
    gap: .0625rem;
    min-width: 0;
    flex: 1;
}

.chip-kicker[b-3uyv2ich9u] {
    font-size: .625rem;
    font-weight: 600;
    letter-spacing: .025rem;
    text-transform: uppercase;
    color: var(--kts-text-muted);
}

.chip-title[b-3uyv2ich9u] {
    font-size: .8125rem;
    font-weight: 600;
    color: var(--kts-text-strong);
    /* One line, ellipsised: the chip is a summary, and a wrapping name would push the whole
       stage down every time a patient has a long name. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* --kts-text-muted, not --kts-text-faint: #9ca3af / #64748b measures 2.54:1 light / 3.42:1 dark on --kts-card-bg — it clears the
   4.5:1 AA body floor in NEITHER theme, so there is no theme in which it is legal for text.
   --kts-text-muted (#6b7280 / #94a3b8) measures 4.83:1 light / 6.35:1 dark on the same surface. Settled in
   AdminStylesheetContrastTests.Text_faint_is_not_usable_for_text_which_is_why_muted_was_chosen;
   these sites were outside band 1's scope. (#1066) */
.chip-meta[b-3uyv2ich9u] {
    font-size: .6875rem;
    color: var(--kts-text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ::deep, and anchored on the scoped root: KtsButton renders its own <button>, which is a CHILD
   of this component's markup and therefore never carries the b-* scope attribute itself. */
.booking-chip[b-3uyv2ich9u]  .chip-action {
    flex-shrink: 0;
}

/* The locked chip's glyph, sitting where the Change button sat (#1519).

   --kts-text-muted, not --kts-text-faint, for the reason the .chip-meta note two rules up already
   records — restated here against THIS rule's own ground, which is --kts-surface (the .booking-chip
   background) rather than the --kts-card-bg that note measures on. A lock is not text, so 4.5:1 is
   not the applicable rule for it (WCAG 1.4.11 asks 3:1 of a non-text control), but it carries the
   same MEANING as the sentence beside it, so the token that already passes the stricter bar was
   taken; it costs nothing and needs no per-theme override.

   NO RATIO IS WRITTEN HERE, deliberately. Every number in this repository that was typed into a
   comment went stale, and a contrast figure goes stale on a Foundation bump nobody re-measures.
   BookingChipLockContrastTests reads the token names out of THIS file and the values out of the
   pinned package, computes the ratio, and carries a control proving the rejected token still
   fails — so the claim moves when the palette does. Run it to learn the numbers.

   No hover, no focus ring, no cursor change: this is a STATE, not a control. Anything that
   reacted to the pointer would re-create the "is it clickable?" question the lock exists to
   answer. */
.chip-lock[b-3uyv2ich9u] {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    color: var(--kts-text-muted);
}
/* /Components/Booking/BookingPageSkeleton.razor.rz.scp.css */
/* #1514 requires the shimmer to be inert under reduced motion, and it IS — but the guard is
   Foundation's, not ours. The pinned package's scoped bundle ships

       @media (prefers-reduced-motion: reduce){
           .kts-skeleton[b-srhf9n2id3],
           .kts-skeleton-text-group .kts-skeleton-text[b-srhf9n2id3]{animation:none}
       }

   and KtsSkeleton.razor's own header says so. This file used to carry a `::deep .kts-skeleton
   { animation: none }` copy, described as "the animation most commonly forgotten on this page".
   It guarded nothing — the rule it duplicated was already in effect — and the test asserting
   the copy was asserting a rule with no work to do, which is the shape of a control that cannot
   fail.

   The copy is gone and AppointmentEditorFrameTests asserts the PACKAGE's guard instead, so the
   criterion is still machine-checked and is checked against the thing that actually delivers it.
   If a Foundation bump drops that media query the test goes red, and a local override belongs
   here again — with a comment naming the version that regressed. */
/* /Components/Booking/BookingSummaryPanel.razor.rz.scp.css */
/* Token-driven throughout (the OverrideModeBanner precedent): every colour comes from a
   Foundation --kts-* token so both themes are correct by construction — no hex, no !important.
   The page stylesheet's hardcoded light-only .summary-* palette is exactly what this component
   must NOT inherit; extraction makes that structural (page-scoped selectors cannot reach the
   panel's subtree). */

.booking-summary-panel[b-h6190knati] {
    height: 100%;
    font-size: .8125rem;
}

/* ── Re-homed column + scroll behaviour ─────────────────────────────────────────────────────
   The page CSS used to hardcode the rail's flex column and the .summary-content scroll region.
   KtsContentCard owns the chrome now; these ::deep rules give the CARD the column and its BODY
   the scroll, while the body's padding stays token-driven (--kts-card-padding — deliberately
   not overridden here). Square corners + a single left border keep the full-height rail look
   the flow always had. */
.booking-summary-panel[b-h6190knati]  .kts-content-card {
    height: 100%;
    display: flex;
    flex-direction: column;
    border-radius: 0;
    border-top: none;
    border-right: none;
    border-bottom: none;
}

.booking-summary-panel[b-h6190knati]  .kts-content-card-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    scrollbar-width: thin;
}

.booking-summary-panel[b-h6190knati]  .kts-content-card-footer {
    flex-shrink: 0;
}

/* ── Receipt sections ── */

/* The net change from `dev` is VERTICAL ONLY: `.5rem 0` → `var(--book-rail-inset) 0`, i.e.
   the rhythm moves from .5rem to 1rem and the horizontal inset stays at zero.

   It is written as a pair rather than a single value because an earlier revision of this
   branch made the padding four-sided, which double-inset every row against
   .kts-content-card-body's own var(--kts-card-padding) — this card is a KtsContentCard
   WITHOUT Padded="false". That broke #1514's "the card pays once" rule and was reverted.
   Saying the double-inset came from `dev` would misdescribe the merged delta: `dev` was
   already `.5rem 0`, and the divider already spanned the card's full width. */
.summary-section[b-h6190knati] {
    padding: var(--book-rail-inset) 0;
    border-bottom: .0625rem solid var(--kts-border-subtle);
}

    .summary-section:first-child[b-h6190knati] {
        padding-top: 0;
    }

    .summary-section:last-child[b-h6190knati] {
        border-bottom: none;
        padding-bottom: 0;
    }

.summary-section-header[b-h6190knati] {
    display: flex;
    align-items: flex-start;
    gap: .5rem;
}

.summary-section-header-with-action[b-h6190knati] {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: .5rem;
}

    .summary-section-header-with-action .summary-section-header[b-h6190knati] {
        flex: 1;
        min-width: 0;
    }

.summary-section-icon[b-h6190knati] {
    font-size: .8125rem;
    color: var(--kts-text-muted);
    margin-top: .0625rem;
    flex-shrink: 0;
    width: .875rem;
    text-align: center;
}

.summary-section-content[b-h6190knati] {
    flex: 1;
    min-width: 0; /* long patient / resource / procedure names wrap instead of widening the rail */
    display: flex;
    flex-direction: column;
    gap: .1875rem;
}

.summary-section-label[b-h6190knati] {
    font-size: .6875rem;
    font-weight: 600;
    color: var(--kts-text-muted);
    text-transform: uppercase;
    letter-spacing: .025rem;
    line-height: 1.2;
}

.summary-value[b-h6190knati] {
    font-size: .8125rem;
    font-weight: 500;
    color: var(--kts-text-strong);
    line-height: 1.4;
    overflow-wrap: anywhere; /* untruncated long values are handled on purpose, not by accident */
}

.summary-detail[b-h6190knati] {
    font-size: .6875rem;
    color: var(--kts-text-muted);
    margin-top: .0625rem;
    display: flex;
    align-items: center;
    gap: .1875rem;
    overflow-wrap: anywhere;
}

    .summary-detail i[b-h6190knati] {
        font-size: .625rem;
    }

/* --kts-text-muted, not --kts-text-faint: #9ca3af / #64748b measures 2.54:1 light / 3.42:1 dark on --kts-card-bg — it clears the
   4.5:1 AA body floor in NEITHER theme, so there is no theme in which it is legal for text.
   --kts-text-muted (#6b7280 / #94a3b8) measures 4.83:1 light / 6.35:1 dark on the same surface. Settled in
   AdminStylesheetContrastTests.Text_faint_is_not_usable_for_text_which_is_why_muted_was_chosen;
   these sites were outside band 1's scope. (#1066) */
/* The designed "not chosen yet" treatment — neutral, not an error and not the old italic
   greyed placeholder. */
.summary-pending[b-h6190knati] {
    font-size: .75rem;
    color: var(--kts-text-muted);
}

/* "Change time" — a real button styled as the inline action it always looked like.

   The ink is --kts-primary stepped one shade toward --kts-text-strong, not --kts-primary itself:
   the plain token measures 5.38:1 LIGHT but 4.34:1 DARK on --kts-card-bg, below the 4.5:1 AA body
   floor, and this is 11px/500 so the body floor is the one that applies. Foundation publishes no
   readable on-surface pair for primary the way it does --kts-warning-text for warning, and #1066
   ruled two sites too few to justify a new Foundation token, so the step is local and derived.
   --kts-text-strong is the theme's opposite pole (#111827 light / #f1f5f9 dark), so the same
   declaration deepens the blue in light and lightens it in dark — the direction has to flip
   because the surface does. Measured: 6.94:1 LIGHT / 5.62:1 DARK on --kts-card-bg, which is the
   panel's backdrop (the component renders inside a KtsContentCard). Idiom precedent:
   ScanCIComponent.razor.css:188. (#1066) */
.summary-change-button[b-h6190knati] {
    background: none;
    border: none;
    padding: 0;
    font-size: .6875rem;
    font-weight: 500;
    color: color-mix(in srgb, var(--kts-primary) 80%, var(--kts-text-strong));
    cursor: pointer;
    white-space: nowrap;
    flex-shrink: 0;
    margin-top: .0625rem;
}

    .summary-change-button:hover[b-h6190knati] {
        text-decoration: underline;
    }

/* ── Alternative-resource pin chips (real buttons) ── */

.summary-chip-list[b-h6190knati] {
    list-style: none;
    margin: .25rem 0 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: .25rem;
}

.summary-pin-chip[b-h6190knati] {
    display: inline-flex;
    align-items: center;
    padding: .125rem .5rem;
    border-radius: 1.25rem;
    font-size: .6875rem;
    font-weight: 500;
    background: var(--kts-surface-sunken);
    color: var(--kts-text-body);
    border: .0625rem solid var(--kts-border-subtle);
    cursor: pointer;
    transition: border-color 0.12s;
}

    .summary-pin-chip:hover[b-h6190knati] {
        border-color: var(--kts-border-solid-hover);
    }

/* ── Footer ── */

.summary-footer-region[b-h6190knati] {
    display: flex;
    flex-direction: column;
    gap: .5rem;
    outline: none; /* programmatic focus target after the banner's explicit exit (#894) */
}

.summary-notes-fields[b-h6190knati] {
    display: flex;
    flex-direction: column;
    gap: .5rem;
}

/* Same one-shade step as .summary-change-button, for the same reason. Plain --kts-danger on
   --kts-danger-soft measures 4.97:1 LIGHT but 4.06:1 DARK, under the 4.5:1 AA body floor.
   Stepped 20% toward --kts-text-strong it measures 6.57:1 LIGHT / 4.90:1 DARK. The BORDER keeps
   the undiluted --kts-danger: it is a 1px rule, non-text, so 1.4.3 does not apply to it and the
   banner keeps reading as danger-coloured at a glance.
   --kts-danger-soft is translucent in dark — rgba(248,81,73,.15) — so the dark figure is measured
   with it composited over --kts-card-bg, the panel's actual backdrop (BookingSummaryPanel renders
   inside a KtsContentCard), never over an implicit white. (#1066) */
.summary-submit-error[b-h6190knati] {
    display: flex;
    align-items: flex-start;
    gap: .5rem;
    padding: .5rem .625rem;
    background: var(--kts-danger-soft);
    border: .0625rem solid var(--kts-danger);
    border-radius: .375rem;
    color: color-mix(in srgb, var(--kts-danger) 80%, var(--kts-text-strong));
}

    .summary-submit-error i[b-h6190knati] {
        font-size: .8125rem;
        flex-shrink: 0;
        margin-top: .0625rem;
    }

.summary-submit-error-text[b-h6190knati] {
    font-size: .6875rem;
    margin: 0;
    line-height: 1.4;
}

/* The named "why Book is disabled" reason — plain copy, deliberately NOT a live region. */
.summary-hint[b-h6190knati] {
    margin: 0;
    font-size: .6875rem;
    color: var(--kts-text-muted);
}

/* Edit mode's status action row (#1235). Wraps rather than scrolls: the rail is a quarter-width
   column and the widest starting status offers three buttons, so a single line would either clip
   them or force a horizontal scrollbar inside a footer band that has none anywhere else.

   The buttons themselves carry no rule here — variant (Primary / Secondary) is the whole of their
   treatment, which is what keeps the hierarchy identical to the one Agenda and the calendar footer
   use for the same hops. The row is FORWARD STEPS ONLY; the destructive action is the standalone
   cancel button below it, which keeps its own rule. */
.summary-status-actions[b-h6190knati] {
    display: flex;
    flex-wrap: wrap;
    gap: .375rem;
}

/* No rule targets the row's buttons themselves. `flex: 0 1 auto` is already every flex item's
   default, so a ::deep rule restating it would be dead CSS that reads as a decision. */

/* Edit mode's cancel action: Ghost KtsButton with danger ink. ::deep because the class lands on
   KtsButton's own <button>, which carries Foundation's scope attribute, not this component's. */
.booking-summary-panel[b-h6190knati]  .summary-cancel-button {
    color: var(--kts-danger);
}
/* /Components/Booking/BookingTimeline.razor.rz.scp.css */
/* Booking "When" step — Timeline pane (#892).
   Layout only: every colour comes from the Foundation --kts-* tokens so the pane
   flips with the app theme, and the ghost/conflict treatment stays the scheduler's. */

/* THE ROOT PAYS NO INSET (#1515 Part 2), and that is the whole of the "padded twice" report.

   `padding: 0.75rem` used to sit here, on the pane ROOT — so a nested SlotStateMessage, which
   carried its own `p-3`, was inset 12 (root) + 12 (its own) = 24px on one axis while the list
   pane's identical message was inset 12px flat. Toggling therefore changed the empty state's
   inset as well as the pane's height. The inset moves onto `.booking-timeline-board`, which
   wraps only the scheduler and its hint; the state message and the edit-mode callout are
   siblings of that board and pay their own, once.

   `min-height` is the floor half of #1515's pane contract — see the note in SlotPicker.razor.css
   for why the cap alone does not stop the toggle moving the day strip above it. */
.booking-timeline[b-jrdv90yevq] {
    /* The envelope AND the scrollport, both on this root — see the note on `.slot-picker`,
       which this mirrors exactly. min + max so the pane is the token whatever it holds
       (padding, gap, the ready-state hint, the edit callout); `overflow-y: auto` so a
       localized SlotStateMessage past the clamp scrolls instead of being clipped. */
    min-height: var(--book-pane-height);
    max-height: var(--book-pane-height);
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: var(--book-cluster-gap);
    width: 100%;
    min-width: 0;
}

/* The board is the pane's one padded child: the scheduler plus the hint under it.

   THE INSET IS HERE AND NOWHERE ABOVE. `dev` carries `padding: var(--book-pane-inset)` on
   `.booking-timeline` itself, and that is the "padded twice" report: a sibling
   SlotStateMessage carrying its own `p-3` was inset root + own on one axis, while the list
   pane's identical message was inset once. Moving the inset down here keeps the pane's
   envelope intact (min + max + `overflow-y: auto` on the root, above) and still insets the
   scheduler, without charging the state message and the edit-mode callout, which are
   siblings of this board and pay their own. */
.booking-timeline-board[b-jrdv90yevq] {
    display: flex;
    flex-direction: column;
    gap: var(--book-cluster-gap);
    min-width: 0;
    padding: var(--book-pane-inset);
}

.booking-timeline[b-jrdv90yevq]  .booking-timeline-scheduler {
    /* HORIZONTAL ONLY, and no height constraint — the root above is the vertical scrollport.

       `overflow-x: auto` on its own is a trap: CSS computes a `visible` value on the other
       axis to `auto` whenever one axis is not visible, so this rule with a constrained height
       would have given the board its OWN vertical scrollbar inside the root's — the nested
       scrolling #1515 is about, where whichever box is under the pointer takes the wheel.
       Dropping `flex`/`min-height` lets the board size to its content, so the promoted
       vertical overflow has nothing to scroll and the root stays the only one that does. */
    overflow-x: auto;
}

.booking-timeline-hint[b-jrdv90yevq] {
    margin: 0;
    font-size: 0.75rem;
    line-height: 1.25rem;
    color: var(--kts-body-color, #374151);
    opacity: 0.75;
}

/* There is deliberately no `.booking-timeline-live` rule any more, and do not re-add one: the
   announcement channel is Foundation's KtsLiveRegion (#974), which hides itself from its OWN
   scoped stylesheet. The rule that used to sit here is the evidence for the swap — it was
   "the same treatment SlotPicker gives its own" in the comment and NOT the same in the CSS,
   having never carried `clip-path` and leaning on the deprecated `clip` alone. */
/* /Components/Booking/CancelAppointmentModal.razor.rz.scp.css */
/* Layout-first, token-driven (the SlotPicker.razor.css standard): no hex except where a
   comment says why, no !important. Replaces the page stylesheet's .cad-* band (#895
   inventory). Notes against what the page carried:

   · The hardcoded 'Segoe UI' font stacks are dropped, as every #877 extraction before this
     one did: the app's font (Inter, via Foundation's --kts-body-font-family) applies.
   · The gray hexes become surface/text tokens, so dark mode is correct by construction —
     the page band had zero dark-mode rules.
   · The page's .cad-textarea / .cad-btn-cancel / .cad-btn-confirm / .cad-sf-dialog rules
     were already deleted as inert (they targeted KtsTextarea/KtsButton/KtsModal internals,
     which never carry this scope attribute); the class names stay on the markup as hooks
     only. */

/* ── Body ───────────────────────────────────────────────────── */
.cad-body[b-mmri0hmtgc] {
    padding: 1.25rem;
    display: flex;
    flex-direction: column;
    gap: 0;
}

/* Warning icon centered above the context banner */
.cad-header-icon[b-mmri0hmtgc] {
    display: flex;
    justify-content: center;
    margin-bottom: .75rem;
}

    .cad-header-icon i[b-mmri0hmtgc] {
        font-size: 1.75rem;
        color: var(--kts-danger);
    }

/* Context banner */
.cad-context-banner[b-mmri0hmtgc] {
    padding: .625rem .75rem;
    background: var(--kts-surface-sunken);
    border-radius: .375rem;
    margin-bottom: 1rem;
}

.cad-context-text[b-mmri0hmtgc] {
    margin: 0;
    font-size: .8125rem;
    color: var(--kts-text-strong);
    line-height: 1.4;
}

.cad-context-sub[b-mmri0hmtgc] {
    color: var(--kts-text-muted);
}

/* Reason label */
.cad-reason-label[b-mmri0hmtgc] {
    display: block;
    font-size: .8125rem;
    font-weight: 600;
    color: var(--kts-text-strong);
    margin-bottom: .5rem;
}

/* ── Chips ──────────────────────────────────────────────────── */
.cad-chips[b-mmri0hmtgc] {
    display: flex;
    flex-wrap: wrap;
    gap: .375rem;
    margin-bottom: .75rem;
}

.cad-chip[b-mmri0hmtgc] {
    font-size: .75rem;
    font-weight: 400;
    color: var(--kts-text-strong);
    background: var(--kts-surface);
    border: .0625rem solid var(--kts-border-subtle);
    border-radius: .375rem;
    padding: .3125rem .625rem;
    cursor: pointer;
    transition: all 0.15s ease;
    line-height: 1.3;
}

    .cad-chip:hover:not(.cad-chip-selected)[b-mmri0hmtgc] {
        background: var(--kts-surface-hover);
        border-color: var(--kts-border-solid);
    }

    .cad-chip.cad-chip-selected[b-mmri0hmtgc] {
        background: var(--kts-danger);
        border-color: var(--kts-danger);
        /* --kts-on-brand (added in KTS.Foundation 0.65.0, kts.foundation#379; still shipped at the
           pinned 0.68.0) is Foundation's ink for a
           saturated brand fill; it is an alias of --kts-counter-fg, whose name hid it from three
           separate readers of this file. It flips with the theme (#ffffff light / #1a1b1e dark)
           BECAUSE --kts-danger does: #c50f1f lightens to #f85149, and fixed white cannot clear AA
           on both. Measured on --kts-danger: 6.07:1 LIGHT and 5.14:1 DARK, against 3.35:1 DARK for
           the #FFFFFF this replaces. The chip label is 12px/600, so the 4.5:1 body floor applies,
           not 3:1. (#1066) */
        color: var(--kts-on-brand);
        font-weight: 600;
    }

        .cad-chip.cad-chip-selected:hover[b-mmri0hmtgc] {
            /* Shaded selected state, derived from the token so a theme retune follows.
               The second colour is --kts-text-strong, not `black`, and that is load-bearing rather
               than cosmetic: hover has to move the fill AWAY from the ink, and the ink now flips
               per theme. --kts-text-strong is Foundation's opposite pole to --kts-on-brand
               (#111827 light / #f1f5f9 dark), so this darkens in light and lightens in dark, which
               is what keeps both themes legible. Measured: 7.81:1 LIGHT / 6.07:1 DARK.
               Mixing toward `black` in both themes would have measured 8.09:1 LIGHT but 3.61:1
               DARK — i.e. it would have turned a hover state that passes today into a failure.
               Precedent for the idiom: ScanCIComponent.razor.css:188. (#1066) */
            background: color-mix(in srgb, var(--kts-danger) 82%, var(--kts-text-strong));
            border-color: color-mix(in srgb, var(--kts-danger) 82%, var(--kts-text-strong));
        }

/* ── Description hint ───────────────────────────────────────── */
.cad-description-hint[b-mmri0hmtgc] {
    padding: .5rem .625rem;
    background: var(--kts-surface-sunken);
    border-radius: .375rem;
    margin-top: .25rem;
}

.cad-hint-text[b-mmri0hmtgc] {
    margin: 0;
    font-size: .6875rem;
    color: var(--kts-text-muted);
    font-style: italic;
    line-height: 1.4;
}

/* ── In-place failure line ──────────────────────────────────── */
/* #1256: a failed cancellation leaves this dialog OPEN and reports here, so the message sits on
   the modal's own surface (--kts-card-bg). --kts-danger on that pair is the same pairing
   .cad-header-icon i already uses and is measured in AdminStylesheetBandTwoContrastTests. */
.cad-error[b-mmri0hmtgc] {
    margin: .75rem 0 0;
    font-size: .8125rem;
    line-height: 1.4;
    color: var(--kts-danger);
}

/* ── Footer ─────────────────────────────────────────────────── */
.cad-footer[b-mmri0hmtgc] {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: .5rem;
    padding: .75rem 1.25rem;
    border-top: .0625rem solid var(--kts-border-subtle);
}
/* /Components/Booking/DateNavigator.razor.rz.scp.css */
/* Layout only — every colour, border, focus ring and dark-mode rule comes from Foundation
   (KtsButton / KtsDatePicker tokens). No hex, no local colours, no !important.

   Moved verbatim from CreateAppointment.razor.css:147-158, which carries ZERO
   prefers-color-scheme / [data-theme] rules — so nothing colour-bearing was safe to bring across,
   and nothing colour-bearing was there to bring.

   NOT MOVED, deliberately: the three `.current-date .e-input-group…` rules that sat below these
   (CreateAppointment.razor.css:160-175). They target Syncfusion input internals, and the field has
   been a Foundation KtsDatePicker for some time — but more decisively, they could never have
   applied at all: Blazor's scoped-CSS attribute is not stamped onto a child component's own
   markup, so a page-scoped descendant selector reaching into KtsDatePicker's DOM matches nothing.
   They were dead in both directions and are deleted rather than relocated. */

/* Layout is Agenda's now and is stated on the markup (`flex items-center gap-1`), so this rule
   carries nothing but the class the component is still identified by. #1515 Part 3.

   THE `.current-date` WRAPPER AND ITS `min-width: 15.625rem` ARE DELETED, not moved. That 250px
   floor is the one #1263 removed from Agenda for the same reason: a floor cannot clip anything —
   it can only make a box wider than its content — so it never protected the long date it claimed
   to. What it DID do was hold 250px of dead space between the trigger and the ▶ chevron whenever
   the day's name was short, which is precisely what a [◀ | date | ▶] cluster must not have. The
   long Romanian date is handled where it is actually handled: Foundation's Navigator variant
   ships `.kts-datepicker.is-navigator { width: auto; max-width: none }` and
   `.kts-datepicker-navtrigger { white-space: nowrap }`, asserted against the pinned package by
   AgendaDatePickerWidthTests.

   `gap: 1rem` becomes `gap-1` on the markup: 16px separated the four Secondary boxes this cluster
   used to be; 4px is what pulls three Ghost controls into one cluster. */
.date-navigation[b-b5et2hf55y] {
    display: flex;
    align-items: center;
}
/* /Components/Booking/DayStrip.razor.rz.scp.css */
/* Layout-first, token-driven (the BookingChip.razor.css standard set after #895 deleted the
   light-only page stylesheet): every colour is a Foundation --kts-* token, so both themes are
   correct by construction — no hex, no !important, no dark-mode block to keep in sync.

   NOTHING HERE IS THE ONLY CARRIER OF A STATE (WCAG 1.4.1). Each chip's accessible name is the
   full sentence (date + count / "closed"), the count is also printed as text on the chip, and the
   selected chip carries aria-current. The colours below are reinforcement, never the message —
   which is also why the closed and unavailable chips differ in ink AND in their printed word, not
   only in hue. */

.day-strip[b-9dgy3th2vd] {
    display: flex;
    align-items: stretch;
    gap: .5rem;
    padding: .5rem .75rem 0;
    min-width: 0;
}

/* The scroll envelope. The window is 7 chips wide, but a narrow viewport (or a long culture-
   formatted "d MMM") must scroll rather than shrink chips below a comfortable hit target. */
.day-strip-days[b-9dgy3th2vd] {
    display: flex;
    align-items: stretch;
    gap: .375rem;
    flex: 1 1 auto;
    min-width: 0;
    overflow-x: auto;
    scrollbar-width: thin;
    padding-bottom: .25rem;
}

.day-chip[b-9dgy3th2vd] {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .125rem;
    flex: 1 1 0;
    /* 2.75rem (44px) is the AAA pointer-target floor; the chips are a primary tap target on tablet, which is
       where this page is actually used at the desk. */
    min-width: 4rem;
    min-height: 3.75rem;
    padding: .375rem .5rem;
    border: .0625rem solid var(--kts-border-subtle);
    border-radius: .625rem;
    background: var(--kts-surface);
    color: var(--kts-text-body);
    cursor: pointer;
    white-space: nowrap;
}

    .day-chip:hover:not(:disabled)[b-9dgy3th2vd] {
        background: var(--kts-surface-hover);
        border-color: var(--kts-border-solid-hover);
    }

    .day-chip:disabled[b-9dgy3th2vd] {
        cursor: default;
        opacity: 0.6;
    }

    /* Selected is a BORDER + FILL change and is also announced through aria-current, so the pair
       "selected chip" / "day with most availability" can never be confused by colour alone. */
    .day-chip.is-selected[b-9dgy3th2vd] {
        border-color: var(--kts-primary);
        border-width: .125rem;
        background: var(--kts-surface-selected);
        /* Compensate the extra border pixel so the row does not jitter as the selection moves. */
        padding: .3125rem .4375rem;
    }

    /* --kts-text-muted, not --kts-text-faint: #9ca3af / #64748b measures 2.43:1 light / 3.15:1 dark on --kts-surface-sunken — it clears the
       4.5:1 AA body floor in NEITHER theme, so there is no theme in which it is legal for text.
       --kts-text-muted (#6b7280 / #94a3b8) measures 4.63:1 light / 5.84:1 dark on the same surface. Settled in
       AdminStylesheetContrastTests.Text_faint_is_not_usable_for_text_which_is_why_muted_was_chosen;
       these sites were outside band 1's scope. (#1066) */
    /* Shut days: sunken and muted, and the meta line says the word. Deliberately NOT disabled —
       #894 keeps a closed day selectable so its "Book anyway" override entry stays reachable. */
    .day-chip.is-closed[b-9dgy3th2vd],
    .day-chip.is-unavailable[b-9dgy3th2vd] {
        background: var(--kts-surface-sunken);
        color: var(--kts-text-muted);
    }

    .day-chip.is-unknown[b-9dgy3th2vd] {
        color: var(--kts-text-muted);
    }

.day-chip-weekday[b-9dgy3th2vd] {
    font-size: .625rem;
    font-weight: 600;
    letter-spacing: .025rem;
    text-transform: uppercase;
    color: var(--kts-text-muted);
}

.day-chip-date[b-9dgy3th2vd] {
    font-size: .8125rem;
    font-weight: 600;
    color: var(--kts-text-strong);
}

/* The count line. Colour reinforces the reading (free / none left / shut) but the words carry it. */
.day-chip-meta[b-9dgy3th2vd] {
    font-size: .6875rem;
    color: var(--kts-text-muted);
}

.day-chip.is-free .day-chip-meta[b-9dgy3th2vd] {
    color: var(--kts-success);
    font-weight: 600;
}

.day-chip.is-full .day-chip-meta[b-9dgy3th2vd] {
    color: var(--kts-text-muted);
}

.day-chip.is-closed .day-chip-meta[b-9dgy3th2vd],
.day-chip.is-unavailable .day-chip-meta[b-9dgy3th2vd] {
    color: var(--kts-text-muted);
}

/* Muted day names/dates inside a shut chip too, so the whole chip recedes as one block. */
.day-chip.is-closed .day-chip-weekday[b-9dgy3th2vd],
.day-chip.is-unavailable .day-chip-weekday[b-9dgy3th2vd],
.day-chip.is-closed .day-chip-date[b-9dgy3th2vd],
.day-chip.is-unavailable .day-chip-date[b-9dgy3th2vd] {
    color: var(--kts-text-muted);
}

/* ::deep, anchored on the scoped root: KtsButton renders its own <button>, a CHILD of this
   component's markup, so it never carries the b-* scope attribute itself. */
.day-strip[b-9dgy3th2vd]  .kts-button {
    flex-shrink: 0;
    align-self: center;
}
/* /Components/Booking/EditPatientBanner.razor.rz.scp.css */
/* Layout-first, token-driven (the SlotPicker.razor.css standard): every colour is a
   Foundation --kts-* token so both themes are correct by construction — no hex, no
   !important. Replaces the page stylesheet's .patient-banner band (#895 inventory).
   Notes against what the page carried:

   · The band read FIVE custom properties this app never defines — --border, --radius-lg,
     --primary, --muted-fg, --foreground. An unresolvable var() with no fallback computes to
     `unset`, so none of them had rendered: the banner had no border, square corners, an
     INVISIBLE avatar (white text on an unset background, since Tailwind's `bg-primary` is
     not a class in this project's theme either) and inherited text colours. Those are
     restored here against real tokens — and the avatar has since become KtsAvatar (#961),
     which paints itself.
   · The .status-badge band (8 hardcoded tint/ink/border triples, plus the only two
     !important declarations in it — a hover opacity + translate) is DELETED, not moved:
     the pill is a KtsBadge now.
   · .chip-icon is dropped: no markup in Admin.Web emits it. */

.patient-banner[b-sfoeiqbxcn] {
    background: var(--kts-surface-sunken);
    border: .0625rem solid var(--kts-border-subtle);
    border-radius: var(--kts-border-radius-lg);
    overflow: hidden;
    flex-shrink: 0;
    margin: 0 var(--book-gutter) var(--book-band-gap);
}

/* ── Identity row ───────────────────────────────────────── */
.banner-top[b-sfoeiqbxcn] {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .75rem 1rem;
}

/* The .avatar circle is deleted with #961: KtsAvatar owns the 36px Md geometry, the rounded-full
   shape, the centring and the shrink-0. The `bg-brand-primary text-white` pair it carried was the
   last non-token colour in this component; the swatch now comes from AvatarPalette, seeded from
   the same display string BookingChipHeader uses so one patient keeps one colour on the page. */

.banner-info[b-sfoeiqbxcn] {
    flex: 1;
    min-width: 0;
}

.banner-name[b-sfoeiqbxcn] {
    font-size: .875rem;
    font-weight: 600;
    color: var(--kts-text-strong);
    /* #1514 edge case: a very long patient name truncates with the title the markup carries,
       rather than wrapping the band to two lines and shoving the stage. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.banner-sub[b-sfoeiqbxcn] {
    font-size: .75rem;
    color: var(--kts-text-muted);
    margin-top: .125rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ── Appointment chips row ──────────────────────────────── */
.banner-bottom[b-sfoeiqbxcn] {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .375rem .625rem;
    padding: .5rem 1rem .625rem;
    border-top: .0625rem solid var(--kts-border-subtle);
}

.banner-label[b-sfoeiqbxcn] {
    font-size: .6875rem;
    font-weight: 700;
    color: var(--kts-text-muted);
    text-transform: uppercase;
    letter-spacing: .03125rem;
}

.banner-chip[b-sfoeiqbxcn] {
    display: flex;
    align-items: center;
    gap: .25rem;
    font-size: .75rem;
    color: var(--kts-text-body);
}

.sep[b-sfoeiqbxcn] {
    color: var(--kts-border-solid);
    font-size: .75rem;
}
/* /Components/Booking/OverrideAttributionNote.razor.rz.scp.css */
/* #1443. Its own stylesheet, and that is not tidiness — Blazor CSS isolation
   scopes every rule to the component that declares it, and this element renders
   inside BookingSummaryPanel but carries THIS component's scope attribute. The
   panel's own `.summary-section` / `.summary-detail` rules therefore cannot
   reach it, so reusing those class names here would have produced an unstyled
   line that looks correct in the markup and wrong on the screen. */

.override-attribution[b-kwqh7ym9p8] {
    display: flex;
    align-items: baseline;
    gap: .3125rem;
    padding: .5rem 0;
    border-bottom: .0625rem solid var(--kts-border-subtle);
    font-size: .6875rem;
}

/* --kts-risk-high-text, the ink the agenda's own override marker uses for its
   icon and label, so the two surfaces read as one signal. Contrast for this
   token on a card surface is measured in both themes by
   AgendaOverrideMarkerContrastTests. */
.override-attribution-icon[b-kwqh7ym9p8] {
    color: var(--kts-risk-high-text);
    font-size: .625rem;
    flex-shrink: 0;
}

/* --kts-text-muted rather than --kts-text-faint: faint (#9ca3af / #64748b)
   measures 2.54:1 light and 3.42:1 dark on --kts-card-bg and clears the 4.5:1
   AA body floor in NEITHER theme, so it is never legal for text. Muted
   (#6b7280 / #94a3b8) measures 4.83:1 and 6.35:1 on the same surface. The same
   choice, for the same reason, as the `.summary-detail` rows beside this one
   (#1066). NO opacity: it would silently reduce a measured ratio in a way a
   stylesheet-reading contrast suite cannot see. */
.override-attribution-text[b-kwqh7ym9p8] {
    color: var(--kts-text-muted);
    overflow-wrap: anywhere;
}
/* /Components/Booking/OverrideChecklist.razor.rz.scp.css */
/* Layout only — no colour, no hex, no !important. State on a checklist row is conveyed by the
   visible localized status WORD, so no row needs a colour of its own and neither theme needs a
   rule here. Moved verbatim from OverrideModeBanner.razor.css with the markup they style (#1518):
   a scoped sheet belongs to exactly one component, so leaving them there would have left the
   modal's copy of this list unstyled — the same defect (.cad-footer) #1518 exists to close. */

.override-checklist[b-wq60tsrwr1] {
    list-style: none;
    margin: .5rem 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: .25rem;
}

.ocl-row[b-wq60tsrwr1] {
    display: flex;
    align-items: baseline;
    gap: .375rem;
    flex-wrap: wrap;
    font-size: 0.75rem;
}

/* The state word leads the row so a screen reader hears "Broken: outside working hours". */
.ocl-state[b-wq60tsrwr1] {
    font-weight: 600;
    white-space: nowrap;
}

.ocl-state[b-wq60tsrwr1]::after {
    content: ":";
}

/* The base rule the ledger recorded as missing: every row carries `.ocl-label`, only a BROKEN one
   also carried `.ocl-label-broken`, and nothing defined the base — so the satisfied rows were
   unstyled while the broken ones were not. `font-weight: 400` is what they were rendering at by
   inheritance; stating it makes the pair a weight contrast rather than one declared half. */
.ocl-label[b-wq60tsrwr1] {
    font-weight: 400;
}

.ocl-label-broken[b-wq60tsrwr1] {
    font-weight: 600;
}

/* Server conflict lines under the resource row — one per conflicting booking. */
.ocl-conflicts[b-wq60tsrwr1] {
    list-style: disc;
    margin: .125rem 0 0;
    padding-inline-start: 1.25rem;
    flex-basis: 100%;
}
/* /Components/Booking/OverrideModeBanner.razor.rz.scp.css */
/* Layout only — every colour, border and dark-mode rule comes from Foundation (KtsCallout's
   Warning tint, KtsTextarea, KtsButton). No hex, no local colours, no !important — the page
   stylesheet's hardcoded palette is exactly what this component must NOT inherit, because it has
   zero dark-theme rules. State on the checklist rows is conveyed by the visible localized status
   WORD, so no row needs a colour of its own. */

.override-banner[b-dciblv2w1b] {
    display: block;
    outline: none; /* focus lands here programmatically; the heading + region role do the announcing */
}

.override-banner-title[b-dciblv2w1b] {
    margin: 0;
    font-size: 0.875rem;
    font-weight: 600;
}

.override-banner-subtitle[b-dciblv2w1b] {
    margin: .25rem 0 0;
    font-size: 0.75rem;
}

/* The .override-checklist / .ocl-* rules moved to OverrideChecklist.razor.css with the markup they
   style (#1518). They could not stay: a scoped stylesheet compiles to a `[b-xxxxx]` attribute
   selector that only this component's own markup carries, so the modal's copy of the list would
   have rendered unstyled — which is exactly the class of defect #1518 was filed for. */

/* The .override-reason-field / -field-label / -hint trio is deleted with #961: KtsFormField owns
   the column layout, the label and the help line (and their spacing). The 10px top margin is the
   only thing left, and it is a Tailwind utility on the field itself. */

/* …with ONE exception. KtsFormField paints its label `text-gray-700` and its help line
   `text-gray-500` — literal Tailwind greys rather than --kts-* tokens. The rules deleted above
   carried NO colour, so both lines inherited `.kts-callout-body`; `inherit` restores exactly that,
   and introduces no local colour and no hex.
   CORRECTED in PR #1874: this comment used to say the two greys had no dark-theme variant
   "anywhere in Foundation 0.60.0", and that the label therefore dropped to ~1.4:1 in this callout.
   Both halves were wrong. kts-foundation-theme.css has carried
   `[data-theme=dark] .text-gray-700{color:#cbd5e1 !important}` and the matching `.text-gray-500`
   (#94a3b8) since at least 0.60.0, and an !important declaration beats this rule whatever its
   specificity — so in the dark theme Foundation's override, not `inherit`, paints both lines, and
   this rule decides the LIGHT theme only. Foundation hardcoding greys is still an upstream
   follow-up; it is a token gap, not a dark-theme legibility hole. */
.override-banner[b-dciblv2w1b]  .kts-field-label,
.override-banner[b-dciblv2w1b]  .kts-field-help {
    color: inherit;
}
/* /Components/Booking/PatientContextTabs.razor.rz.scp.css */
/* Layout-first, token-driven (the SlotPicker.razor.css standard): every colour is a
   Foundation --kts-* token so both themes are correct by construction — no hex, no
   !important. Notes against the page stylesheet bands these rules replace
   (.patient-details-tabs band and the .assignments-container band, see the #895
   inventory):

   · The page's `min-height: 250px !important` on the container fought nothing that still
     exists — stated once, without !important.
   · The hardcoded 'Segoe UI' font stacks are dropped, as every #877 extraction before
     this one did: the app's font (Inter, app.css) applies.
   · The page declared .section-card/.section-title twice (a generic band at :286 and the
     tabs band at :826 — same specificity, later wins); only the WINNING declarations were
     carried, so the rendered result is unchanged.
   · Scrollbar colours keep Foundation's neutral ramp via tokens instead of the page's
     gray hexes. */

.patient-details-tabs[b-zk0z2a0385] {
    /* `flex: 0 0 calc(50% - 6px)` until #934: this panel used to share the patient step's column
       with the result table, and the two split it down the middle. It sits under the PROCEDURE
       picker now and shares nothing, so a fixed half-height would leave the tabs stunted with
       dead space under them. It takes what it needs, floored by min-height below. */
    flex: 0 1 auto;
    border: .0625rem solid var(--kts-border-subtle);
    border-radius: .625rem;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    background: var(--kts-surface);
    min-height: 15.625rem;
}

/* ── Tab strip + panes — KtsTabs (#963) ──────────────────────────────────────────────────────
   The hand-rolled .tabs-header / .tab-btn (+ :hover, .active, i) / .tabs-content / .tab-pane
   (+ .active) bands are DELETED, not orphaned: KtsTabs draws the strip and owns the panel
   element, so what is left below is only the height/scroll chain this card needs, in the
   ::deep .patient-context-tabs form PatientComponent.razor.css already uses for the same
   primitive.

   Deliberately NOT restyled: the trigger colours. Foundation paints them with Tailwind
   text-gray-500/700/900 rather than --kts-* tokens, and #963 checked that _dark-mode.scss
   overrides all three under [data-theme] — so both themes are correct, just not token-driven.
   The active tab therefore reads near-black with a --kts-primary underline where the old
   .tab-btn.active was primary-coloured and bold. That is Foundation's tab look, which this
   app already shows on PatientComponent; fixing the token gap is an upstream job. */

/* The card is a flex column; the tabs must be the growing child, and min-height:0 is what
   lets the panel actually scroll instead of stretching the card. (Replaces .tabs-content's
   `flex: 1; min-height: 0`.) */
[b-zk0z2a0385] .patient-context-tabs {
    flex: 1 1 auto;
    min-height: 0;
}

[b-zk0z2a0385] .patient-context-tabs .kts-tabs-list {
    flex-shrink: 0;
}

/* The panel is the scroll container now — for the eye AND for infinite-scroll.js, which picks
   the sentinel's IntersectionObserver root by walking up to the nearest computed overflow-y
   that is neither visible nor hidden. Foundation already ships overflow-y:auto here; the cap
   and the padding are the old .tab-pane's `max-height: 250px` and .tabs-content's 12px.
   Foundation's own `padding: 1rem 0` is overridden by the shorthand.

   274px, not 250px, and the extra 24px is not a fudge. The two declarations used to sit on
   DIFFERENT elements — .tabs-content owned `padding: 12px`, and .tab-pane owned
   `max-height: 250px` INSIDE that padding — so the scrollable viewport was 250px and the band
   it occupied was 274px. Collapsing both onto one element puts them under Tailwind preflight's
   `box-sizing: border-box`, where a 250px cap would swallow the padding and leave a 226px
   viewport: a card 24px shorter and 24px less of the plans list visible per scroll. 250 + 12 +
   12 restores both exactly, which is what "the card's height is unchanged" has to mean. */
[b-zk0z2a0385] .patient-context-tabs .kts-tabs-panel {
    padding: .75rem;
    max-height: 17.125rem;
    scrollbar-width: thin;
}

    [b-zk0z2a0385] .patient-context-tabs .kts-tabs-panel::-webkit-scrollbar {
        width: .25rem;
    }

    [b-zk0z2a0385] .patient-context-tabs .kts-tabs-panel::-webkit-scrollbar-track {
        background: transparent;
    }

    [b-zk0z2a0385] .patient-context-tabs .kts-tabs-panel::-webkit-scrollbar-thumb {
        background: var(--kts-border-solid);
        border-radius: .625rem;
    }

/* The plans pane's own stacking — .tab-pane's `display: flex; flex-direction: column; gap: 8px`
   moved down one level onto the wrapper that carries aria-busy. It lives HERE rather than on
   .kts-tabs-panel on purpose: the panel's `display` is how KeepPanelsMounted hides the inactive
   panels (`hidden` → display:none), and an author rule setting display on that element is
   exactly the way to break it. */
.plans-pane[b-zk0z2a0385] {
    display: flex;
    flex-direction: column;
    gap: .5rem;
}

/* --kts-text-muted, not --kts-text-faint: #9ca3af / #64748b measures 2.54:1 light / 3.42:1 dark on --kts-card-bg — it clears the
   4.5:1 AA body floor in NEITHER theme, so there is no theme in which it is legal for text.
   --kts-text-muted (#6b7280 / #94a3b8) measures 4.83:1 light / 6.35:1 dark on the same surface. Settled in
   AdminStylesheetContrastTests.Text_faint_is_not_usable_for_text_which_is_why_muted_was_chosen;
   these sites were outside band 1's scope. (#1066) */
.tabs-empty[b-zk0z2a0385] {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 2rem 1rem;
    color: var(--kts-text-muted);
    font-size: .75rem;
    text-align: center;
    gap: .5rem;
}

    .tabs-empty i[b-zk0z2a0385] {
        font-size: 1.75rem;
        opacity: 0.3;
    }

/* ── Appointment-history sections ───────────────────────── */
.assignments-container[b-zk0z2a0385] {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    padding: .5rem;
}

.section-card[b-zk0z2a0385] {
    background: var(--kts-surface);
    border: .0625rem solid var(--kts-border-subtle);
    border-radius: .75rem;
    overflow: hidden;
}

.section-header[b-zk0z2a0385] {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1.25rem 1.5rem;
    border-bottom: .0625rem solid var(--kts-border-subtle);
}

.section-title[b-zk0z2a0385] {
    font-size: 1.125rem;
    font-weight: 600;
    color: var(--kts-text-strong);
    margin: 0;
}

/* Three rules reach into KtsAccordionItem, and all three exist because the primitive is being used
   as a SECTION CARD HEADER here rather than as one row of a KtsAccordion — which is what it is
   styled for. They replace 22 lines (.section-toggle, :hover, .toggle-icon) that existed solely to
   make a <button> look like the <div> it had replaced. */

/* 1. The item draws a bottom hairline as an inter-item separator. Past/Cancelled each hold ONE
      item inside a .section-card, so that hairline lands exactly on the card's own bottom border
      and reads as a doubled line. */
.section-card[b-zk0z2a0385]  .kts-accordion-item {
    border-bottom: 0;
}

/* 2. Header metrics, so the three history cards are visual peers. The accordion trigger ships
      .875rem/1rem padding at .9375rem type; .section-header/.section-title (still rendered by the
      Future card above, which has no disclosure and therefore no accordion) are 20px/24px at 18px.
      Left alone, the swap would ship one card with a noticeably larger header than the two beneath
      it. Matching the primitive to the existing pair is the smaller change — and the alternative,
      restyling the Future card down to the primitive's scale, is a design decision this issue
      explicitly puts out of scope. Colour is deliberately NOT touched: the trigger's own
      --kts-heading-color is right in both themes. */
.section-card[b-zk0z2a0385]  .kts-accordion-trigger {
    padding: 1.25rem 1.5rem;
    font-size: 1.125rem;
}

/* 3. …and the body's inline padding with it, so an appointment card in Past/Cancelled lines up on
      the same left edge as one in Future (.section-body is 16px 24px; the primitive's region-inner
      is 0 1rem). Longhands only — the block padding is the primitive's, and it is what animates the
      open/close transition. */
.section-card[b-zk0z2a0385]  .kts-accordion-region-inner {
    padding-left: 1.5rem;
    padding-right: 1.5rem;
}

.section-body[b-zk0z2a0385] {
    padding: 1rem 1.5rem;
}

.appointments-list[b-zk0z2a0385] {
    display: flex;
    flex-direction: column;
    gap: .75rem;
}

.empty-text[b-zk0z2a0385] {
    font-size: .875rem;
    color: var(--kts-text-muted);
    font-style: italic;
    margin: 0;
}
/* /Components/Booking/PatientPicker.razor.rz.scp.css */
/* Layout-first, token-driven (the SlotPicker.razor.css standard): every colour is a
   Foundation --kts-* token so both themes are correct by construction — no hex, no
   !important. Notes against the page stylesheet these rules replace:

   · Declarations that read the page's undefined custom properties (--input-bg, --radius,
     --muted-bg, --foreground, --muted-fg, --border-color) are DROPPED, not translated:
     an invalid var() computes to `unset`, so none of them has rendered for a long time —
     dropping them is the visually-neutral move (see the #895 inventory).
   · The two !important fights are resolved structurally: the 310px selected-state cap is
     a compound selector instead of `!important` over the 620px base, and the header row's
     margin is stated once instead of `0 !important` against a second rule in the same file.
   · The near-white header hex (#faf9f8) maps to --kts-surface-sunken (#f9fafb) — the one
     deliberate off-by-a-hair substitution, taken for dark-mode correctness. */

.patient-header-row[b-s1gz29bry7] {
    display: flex;
    align-items: center;
    gap: .625rem;
    flex-shrink: 0;
    margin-bottom: 0;
}

/* ── Search input ───────────────────────────────────────── */
/* Layout only. The `position: relative` here and the absolutely-positioned `.search-icon`
   it anchored were the hand-rolled magnifier; both are deleted in #973, because
   KtsTextInput's LeadingIcon now owns the glyph, its inline-start offset and the input's
   matching text inset together. What remains is not adornment plumbing: this div is the
   flex child that claims the header row's spare width beside "Add patient", and the
   primitive gives the consumer no handle on its own wrapper to carry that.

   `min-width: 0` is new and deliberate. `flex: 1` alone leaves min-width at `auto`, which
   floors a flex item at its content's min-content width — for an <input> that is the UA's
   default ~20-character size, so on a narrow viewport the row could overflow rather than
   let the search box shrink. Nothing about the adornment caused that; the adornment's
   2.25rem inset makes the floor higher, which is why it is worth fixing here. */
.patient-search-wrap[b-s1gz29bry7] {
    flex: 1;
    min-width: 0;
}

.patient-add-wrap[b-s1gz29bry7] {
    flex: 0 0 auto;
    display: flex;
    justify-content: flex-end;
}

/* ── Table container ────────────────────────────────────── */
.patient-grid-wrap[b-s1gz29bry7] {
    flex: 1;
    border: .0625rem solid var(--kts-border-subtle);
    border-radius: .625rem;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    min-height: 0;
    /* The `transition: flex 0.2s ease` here animated the has-selection shrink (#934 removed it).
       Nothing changes this element's flex any more, so the transition had nothing left to ease. */
}

/* THE SCROLLPORT, and since #975 that is a contract rather than an implementation detail.
   DataTable is passed ExternalScrollContainer="true", which makes it emit NO overflow wrapper
   of its own — so this element is what its sticky <thead> resolves against, and what
   infinite-scroll.js lands on when it walks up from the sentinel. Two consequences worth
   stating before someone "tidies" this rule:

   · `overflow: auto`, not `overflow-y: auto`. Dropping DataTable's internal wrapper also drops
     the horizontal scrolling a wide table relies on, so this container owns BOTH axes.
   · A HEIGHT CONSTRAINT is what makes the sticky header mean anything. Without one there is no
     scrolling box, and a sticky header with nothing to stick to is inert — the exact failure
     mode kts.foundation#337 was filed for.

   That constraint used to be `max-height: 38.75rem`, a 620px viewport guess (#1515 Part 1).
   ServicePicker carried the same mistake at 16.25rem, so the two pickers disagreed by 240px on
   the same stage — one wasting the bottom half of a large monitor, the other overflowing a
   small one. The constraint is now inherited, and NOT re-stated as a token either: `dev`
   briefly carried `max-height: var(--book-pane-height)` here, on the reasoning that nothing
   above the picker was bounded and BookingPageSkeleton had reserved that token for this
   scrollport. #1515 Part 1 removed the premise — the stage is `flex-1 min-h-0
   overflow-hidden` on every route now, so this element always has a bounded ancestor to size
   against, and a cap on top of it would simply reimpose the disagreement the literals caused:
   on a tall monitor the picker would stop at the token while the stage kept going. This
   element takes the height the bounded stage gives its flex column, so the scrolling box (and
   therefore the sticky header) still exists and is the right size on every screen.

   `min(18rem, 100%)` is a floor that cannot overflow its own parent — 18rem where there is
   room, the whole available box where there is not — so the table never collapses to a
   two-row strip and never grows taller than the box containing it. `min-height: 0` on the
   `.patient-grid-wrap` above is what lets this shrink at all: a flex item's automatic minimum
   size is its content, and a table's content is tall. */
.patient-scroll-container[b-s1gz29bry7] {
    position: relative;
    width: 100%;
    flex: 1 1 auto;
    min-height: min(18rem, 100%);
    overflow: auto;
}

/* ── Table ──────────────────────────────────────────────── */
/* The whole `.patient-table` block is gone with #975. Foundation's DataTable owns the table
   now, and scoped CSS cannot reach RCL-rendered markup, so keeping these rules would have left
   dead declarations that merely looked authoritative. Their behaviour is not lost: the sticky
   header, the pointer cursor, the token-based hover (--kts-surface-hover), the selected fill
   (--kts-surface-selected) and the focus ring — `outline: 2px solid var(--kts-primary);
   outline-offset: -2px`, byte-identical to the rule that stood here — all ship in
   kts-foundation-theme.css under `.kts-dt-row*`. The deltas that ARE real (cell typography,
   row height, header fill) are enumerated in the .razor header. */

/* ── The picker's own loading / error surface (#1120) ────── */
/* Layout, plus ONE colour — and the split is not arbitrary. KtsSpinner and KtsErrorState own
   everything inside themselves, and scoped CSS could not reach it even if this file tried (the
   #975 note below says the same of .patient-table). What is left to this file is the box the two
   states share — centred, with enough room that the wait reads as a state of the list rather than
   as a stray glyph under the header — and the ONE element that is genuinely ours: the caption
   beside the spinner, which is a plain <p> in this component's own markup because KtsSpinner
   renders its Label as an accessible name only, never as visible text. That <p> therefore has no
   primitive to inherit from, so it takes --kts-text-muted here, the same token the sibling captions
   in this folder use. A --kts-* token rather than a hex, so both themes are correct by
   construction; no other declaration below sets a colour.

   The padding is a compromise the IntersectionObserver has a stake in: this block sits between the
   table and the sentinel, so its height pushes the sentinel down inside the scrollport (a fixed
   620px until #1515 made it the stage's own height, floored at 18rem — the floor is what keeps
   this argument true on a short viewport rather than only on a tall one). Too
   tall on an empty list and the sentinel leaves the box, the observer never fires, and NOTHING is
   ever fetched — the failure mode #1120 §3 names. 2.5rem plus the spinner keeps it far inside. */
.patient-load-state[b-s1gz29bry7] {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .75rem;
    padding: 2.5rem 1rem;
}

.patient-load-state-text[b-s1gz29bry7] {
    margin: 0;
    font-size: .875rem;
    color: var(--kts-text-muted);
}

/* ── Sentinel ───────────────────────────────────────────── */
/* A sibling of DataTable inside the scrollport rather than a <tr> in its <tbody>. The padding
   is unconditional — the same 1rem the `.sentinel-row td` rule it replaces applied whether or
   not the spinner was up — and that is worth keeping rather than collapsing to zero when idle:
   a sentinel that GROWS at the moment it becomes visible changes the scroll geometry mid-fetch,
   under the very IntersectionObserver that is watching it. */
.patient-sentinel[b-s1gz29bry7] {
    display: flex;
    justify-content: center;
    padding: 1rem 0;
}
/* /Components/Booking/ResourceSelectors.razor.rz.scp.css */
/* Layout only — every colour, border, focus ring and dark-mode rule comes from Foundation
   (KtsFormField / KtsSelect / KtsButton tokens). No hex, no local colours, no !important.

   This matters more than usual here: the block this component replaces was styled from
   CreateAppointment.razor.css, which contains ZERO prefers-color-scheme / [data-theme] rules and
   hardcodes its palette — so anything copied across verbatim would have been wrong in dark theme.

   Only elements authored in ResourceSelectors.razor are styled here: Blazor's scoped CSS attribute
   is never applied to a child component's own markup, so nothing rendered by KtsFormField,
   KtsSelect or KtsButton can be reached from this file by design. */

.resource-selectors[b-wfl0gi3amh] {
    display: flex;
    flex-direction: column;
    gap: .75rem;
}

/* Single-column stack, one row per required resource type. See the PR note on the dead two-column
   .col-6 rules left behind in the page stylesheet: restating that layout would be a design
   decision, and this component preserves what actually rendered. */
.resource-selector-row[b-wfl0gi3amh] {
    display: block;
}

/* Select and its clear button on one line, the select taking the slack. */
.resource-selector-control[b-wfl0gi3amh] {
    display: flex;
    align-items: center;
    gap: .5rem;
}

/* min-width:0 lets the select shrink inside the flex row instead of overflowing the card when a
   resource name is long — the accessible name stays intact either way, nothing is truncated. */
.resource-selector-select[b-wfl0gi3amh] {
    flex: 1 1 auto;
    min-width: 0;
}
/* /Components/Booking/ServicePicker.razor.rz.scp.css */
/* Layout-first, token-driven (the SlotPicker.razor.css standard): every colour is a
   Foundation --kts-* token so both themes are correct by construction — no hex, no
   !important. The page stylesheet's light-only hex palette for these rules was NOT
   carried across. Only elements authored in ServicePicker.razor are styled here;
   KtsTextInput / KtsBadge / KtsEmptyState internals are Foundation's — the field label rule
   below is the ONE exception, and it is scoped ::deep precisely because Blazor's scope
   attribute never reaches a child component's own markup. */

/* ── The field label (#983) ──────────────────────────────────────────────────────────────────
   .field-label / .service-picker-label / .required are DELETED: KtsFormField owns the label, the
   label-to-input association and the required mark, so all three were dead. Part of their look goes
   with them, deliberately — Foundation's `text-sm font-medium` label is what this stage's other
   labelled fields (ResourceSelectors) already show, and one stage with two label styles was the
   accident rather than the design.

   What is kept is the INK. Foundation paints the label `text-gray-700`: a literal Tailwind grey
   rather than a --kts-* token. (Corrected in PR #1874, re-checked on 0.75.0: this used to say the
   grey had no dark-theme variant. The theme stylesheet DOES carry `[data-theme=dark] .text-gray-700
   { color: #cbd5e1 !important }`, and has since at least 0.60.0, so in dark mode that override —
   not this rule — paints the label, and this rule's token governs the light theme only.
   BookingSearchFieldNamesTests pins the override, and OverrideModeBanner.razor.css carries the same
   correction.)

   THE ANCHOR MUST BE AN ELEMENT THIS COMPONENT WRITES, which is why the markup wraps the field in
   <div class="service-picker-field">. Blazor stamps the scope attribute on the LAST compound before
   ::deep, and only on markup authored here — so a class handed to KtsFormField's Class parameter
   would sit on the child's own div, scoped to Foundation, and this rule would compile to
   `.service-picker-field[b-…] .kts-field-label`: a selector that matches nothing, with no warning.
   Copilot caught exactly that in review round 1, and ScopedCssDeepReachTests now models it.

   Nothing else is overridden: size, weight, case and the label's own margin are the primitive's,
   and the few pixels of spacing that moves are a browser check that is OWED, not claimed. The ink
   stays --kts-text-muted, never --kts-text-faint, which BookingInkDeclarationTests bans here. */
.service-picker-field[b-yp51j4wwzz]  .kts-field-label {
    color: var(--kts-text-muted);
}

/* ── Search ──
   No rules: the search box is a bare <KtsTextInput LeadingIcon="search" /> since #973.
   The `.search-wrap` positioning context and the absolutely-positioned `.search-icon`
   that lived here were the hand-roll Foundation 0.61.0 replaced — the glyph, its
   inline-start offset and the input's matching text inset are all the primitive's now,
   and none of the three can drift from the other two any more. Deleting the wrapper
   costs nothing: it only ever existed to be `position: relative`, and the primitive's
   own wrapper (display:block; width:100%) is the flex-column child in its place. */

/* ── List ──
   ONE SCROLLPORT PER STAGE, AND IT IS THIS ONE (#1515 Part 1).

   The `max-height: 16.25rem` that stood here was a viewport guess: 260px on every screen,
   so the list showed the same eight rows on a 13" laptop and on a 27" monitor with the rest
   of the stage left empty below it. PatientPicker made the same mistake with a different
   literal (38.75rem), which is why the two pickers disagreed by 240px on the same stage.

   The list now consumes the height it is GIVEN. `min-height: 0` on the wrapper is
   load-bearing rather than tidy: a flex item's automatic minimum size is its CONTENT, so
   without it this column grows past the viewport, the stage never gets a bounded height to
   scroll inside, and the list has nothing to size against — which is exactly why a literal
   was here in the first place. Removing the literal without the min-height would restore
   the page-level scroll, not fix it.

   `min(18rem, 100%)` is a floor that cannot itself overflow: 18rem where there is room, the
   whole available box where there is not, so a short stage yields a short list rather than a
   list taller than the box containing it.

   `overflow: visible` on the wrapper, not `hidden`: the wrapper is no longer a clipping box,
   it is the flex column that hands its height down. Clipping here would hide the very rows
   the list is now tall enough to show. */
.service-list-wrap[b-yp51j4wwzz] {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow: visible;
}

.service-list[b-yp51j4wwzz] {
    flex: 1 1 auto;
    min-height: min(18rem, 100%);
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: .25rem;
    scrollbar-width: thin;
}

/* ── Service card ── */
.service-card[b-yp51j4wwzz] {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    padding: .5rem .625rem;
    background: var(--kts-surface);
    border: .0625rem solid var(--kts-border-subtle);
    border-radius: .375rem;
    cursor: pointer;
    text-align: left;
    transition: all 0.15s ease;
    font-family: inherit;
    flex-shrink: 0;
    color: inherit;
}

    .service-card:hover:not(.selected)[b-yp51j4wwzz] {
        background: var(--kts-surface-hover);
        border-color: var(--kts-border-solid-hover);
    }

    .service-card.selected[b-yp51j4wwzz] {
        background: var(--kts-surface-selected);
        border: .125rem solid var(--kts-primary);
    }

.card-left[b-yp51j4wwzz] {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex: 9;
    min-width: 0;
}

.card-info[b-yp51j4wwzz] {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: .125rem;
}

.card-name-row[b-yp51j4wwzz] {
    display: flex;
    align-items: center;
    gap: .375rem;
    flex-wrap: wrap;
}

.card-name[b-yp51j4wwzz] {
    font-size: .8125rem;
    font-weight: 600;
    color: var(--kts-text-strong);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    flex: 1;
    min-width: 0;
}

/* --kts-text-muted, not --kts-text-faint: #9ca3af / #64748b measures 2.54:1 light / 3.42:1 dark
   on --kts-surface (the resting card) — it clears the 4.5:1 AA body floor in NEITHER theme, so
   there is no theme in which it is legal for text. --kts-text-muted measures 4.83:1 light /
   6.35:1 dark on the same surface. Settled in
   AdminStylesheetContrastTests.Text_faint_is_not_usable_for_text_which_is_why_muted_was_chosen.

   One composite state is still short: on a SELECTED card (.service-card.selected fills with
   --kts-surface-selected) the same ink measures 4.44:1 LIGHT / 5.29:1 dark — 0.06 under the
   floor in light only, up from 2.33:1 before this change. The remaining shortfall belongs to the
   tinted surface, not to the ink: --kts-text-muted is already Foundation's lightest text token
   that clears AA anywhere, and DayStrip's .day-chip-meta has carried the same 4.44:1 on a
   selected chip since before this change. Pinned by name in
   AdminStylesheetBandTwoContrastTests.BelowAaPairs and left for its own issue rather than
   answered with a third local ink here. (#1066) */
.card-meta[b-yp51j4wwzz] {
    display: flex;
    align-items: center;
    gap: .25rem;
    flex-wrap: wrap;
    font-size: .6875rem;
    color: var(--kts-text-muted);
}

.dot[b-yp51j4wwzz] {
    color: var(--kts-border-solid);
}

.card-right[b-yp51j4wwzz] {
    display: flex;
    align-items: center;
    gap: .375rem;
    flex-shrink: 0;
    flex: 1;
    justify-content: end;
}

.card-price[b-yp51j4wwzz] {
    font-size: .8125rem;
    font-weight: 700;
    color: var(--kts-text-strong);
    white-space: nowrap;
}

/* ── Legend ── */
.legend[b-yp51j4wwzz] {
    display: flex;
    align-items: center;
    gap: 1rem;
}

.legend-item[b-yp51j4wwzz] {
    display: flex;
    align-items: center;
    gap: .3125rem;
    font-size: .6875rem;
    color: var(--kts-text-muted);
}
/* /Components/Booking/SlotPicker.razor.rz.scp.css */
/* Layout only — every colour, border and dark-mode rule comes from Foundation
   (KtsTimeSlots / KtsEmptyState / KtsCallout tokens). Adding local colours here
   would have to be duplicated for the dark theme and would drift.

   Only elements authored in SlotPicker.razor are styled here: Blazor's scoped
   CSS attribute is never applied to a child component's own markup, so anything
   rendered by SlotStateMessage or by Foundation uses utility classes instead. */

/* THE PANE IS THE SAME BOX WHATEVER IS IN IT — #1515 Part 2's criterion, on the element
   #1514's review rounds established is the only one that can keep it.

   WHY A FLOOR AND NOT JUST A CAP (#1515). #1514 made the two panes' CAPS agree, which stops
   the card resizing when their LIMITS differ — but the reported defect is the day strip and
   the List/Timeline toggle row moving when the user toggles, and that happens whenever the
   panes' CONTENT differs. A two-slot day is nowhere near either cap, so capping both at
   34rem changes nothing about it. The bar was never resizing; the layout was reflowing.

   WHY HERE, ON THE ROOT (#1514). `.slot-picker-grid` is rendered only
   `@if (_hasRenderableSlots)`, which is true whenever the day HAS slots at all — INCLUDING
   AllUnavailable, whose whole definition is "slots exist, none bookable" (BookingSlotState.cs).
   So "the grid does not exist and SlotStateMessage renders in its place" is true only for
   ClosedDay / NoSlots / NoResources / NotConfigured / Error — the states where the day genuinely
   produced nothing — and was FALSE for AllUnavailable, where the grid stayed mounted while
   SlotStateMessage ALSO rendered a full-bleed KtsEmptyState into the same box: the overlap and
   dead disclosure-row click #1786 reported. #1786 fixed the rendering (SlotStateMessage renders
   a compact KtsCallout banner for AllUnavailable, ordered BEFORE the grid in markup, not a
   full-bleed empty state replacing it) and this paragraph is the correction to the premise it
   was reasoned under.

   That correction does not change the floor's placement. The floor still belongs on THIS root
   and not on `.slot-picker-grid`, because the grid is still absent on every state that collapses
   worst (ClosedDay/NoSlots/NoResources/NotConfigured/Error all render nothing else), and a floor
   scoped to an element that doesn't exist on exactly those days would still guarantee nothing.
   On AllUnavailable — the one state where the grid AND a banner both render — the grid keeps its
   own `flex: 1 1 auto; min-height: 0` below and simply receives less of the root's fixed height
   once the banner's natural (non-flex) height is subtracted from it; the root's floor + cap +
   `overflow-y: auto` trio is what keeps that combination from overflowing or clipping, exactly as
   it already does for a single long-content day. Nothing here assumes the grid and the banner are
   mutually exclusive.

   WHY `overflow-y: auto` AND NOT `hidden`. The states that render directly under this root
   have no scrolling child to escape into. A fixed height with `hidden` clipped a localized
   SlotStateMessage outright once its title, subtitle and action passed the 18rem clamp,
   which text zoom reaches easily. One scrollport, here, satisfies both halves: nothing is
   clipped and nothing exceeds the token — and it puts the pane's only scrollport on the
   pane, which is where #1515 wants it. */
.slot-picker[b-d04gd83lq3] {
    display: flex;
    flex-direction: column;
    min-height: var(--book-pane-height);
    max-height: var(--book-pane-height);
    overflow-y: auto;
}

/* Same scroll envelope the old .slots-grid had, so a long day still fits inside
   the card instead of pushing the summary panel off screen.

   The envelope is --book-pane-height, not a literal. It was 26.25rem here and 28rem
   on the timeline board, so the two panes differed by 28px and the skeleton — which
   draws at the token — could match neither (#1514).

   NEITHER THE FLOOR NOR THE CAP IS HERE, and that is the resolution of #1514's and
   #1515's two attempts at this box. Both live on `.slot-picker` above, which is also the
   pane's only scrollport. A floor here would apply to an element that does not exist on a
   day with no slots AT ALL — ClosedDay / NoSlots / NoResources / NotConfigured / Error —
   exactly the days the collapse is worst. (Not "no bookable slots": AllUnavailable means
   slots exist and none are bookable, this element still renders for it, and conflating the
   two was the premise error #1786 corrected in the note on `.slot-picker` above.) A second
   scrollable box here would also be the nested-scrollport defect #1515 is about, where
   whichever box is under the pointer takes the wheel.

   The inset IS here, as --book-pane-inset rather than a literal: this is a DIRECT child of
   a card that is Padded="false" precisely so each child pays once, and the skeleton draws
   at the token, so a `.75rem` left behind would diverge the moment the scale is tuned.
   What #1515 still owns is the double-inset SlotStateMessage — a component this sheet
   cannot reach, because Blazor's scoped attribute never lands on a child component's
   markup. */
.slot-picker-grid[b-d04gd83lq3] {
    /* `min-height: 0` is load-bearing: a flex item's automatic minimum size is its CONTENT,
       so without it a long day pushes the root past the envelope the root just fixed. */
    flex: 1 1 auto;
    min-height: 0;
    padding: var(--book-pane-inset);
}

/* The same inset as the grid it stands in for, deliberately. #1515 asks for `padding: 0`
   here on the reasoning that "the parent pays" — but the parent is `.slot-picker`, which
   pays nothing (the card is Padded="false" and each child insets itself), and this block
   is not nested inside `.slot-picker-grid`: it REPLACES it for the length of the fetch.
   Zeroing it would inset the skeletons at 0 and the grid that succeeds them at 12, i.e.
   a layout shift at hydration — the exact defect #1514 exists to remove — so the two
   states are stated at the one token instead. */
.slot-picker-skeletons[b-d04gd83lq3] {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(8.125rem, 1fr));
    gap: .375rem;
    padding: var(--book-pane-inset);
}

.slot-picker-skeleton[b-d04gd83lq3] {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 2.375rem;
    padding: .5rem;
    border-radius: .5rem;
}

/* There is deliberately no `.slot-picker-live` rule any more, and do not re-add
   one: the live region is Foundation's KtsLiveRegion (#974), which carries the
   hiding technique in its OWN scoped stylesheet. So there is no per-pane copy to
   drift and nothing here for a Tailwind tree-shake to remove — which is the whole
   benefit, and the extent of it. It is NOT a guarantee the region cannot be
   un-hidden: scoped CSS compiles to an ordinary `[b-xxx]` attribute selector, so a
   `::deep` rule from an ancestor or a high-specificity global rule still wins. If
   the announcement ever becomes visible on screen, look for that, not for a
   missing rule in this file.
   A per-pane copy is precisely how this file and BookingTimeline.razor.css
   drifted — the timeline's copy never carried `clip-path` at all and leaned on
   the deprecated `clip` alone. */
/* /Components/ComingSoonComponent.razor.rz.scp.css */
/* ────────────────────────────────────────────────────────────────────────────
   Coming-soon placeholder — Foundation-conformant (#1053, band 1 of 3).

   Tokens for ink and chrome, `rem` for every length. Ratios are WCAG 2.1
   relative luminance against `--kts-card-bg` (#ffffff light / #1f2024 dark),
   or against the tinted fill named in the rule, computed by
   `tools/css-audit/css_theme_audit.py`.

   ── A PRE-EXISTING BUG THIS CHANGE DELIBERATELY DOES NOT FIX ──
   Everything from `.sparkles-icon` down is NESTED inside `.icon-circle i`, so
   it compiles to `.icon-circle i .sparkles-icon`, `.icon-circle i
   .coming-soon-badge`, and so on. In ComingSoonComponent.razor those elements
   are siblings of `.icon-circle`, never descendants of the `<i>` inside it,
   so the whole nested block matches nothing today. Un-nesting it would make
   nine rules start applying at once and visibly change the component in light
   theme, which #1053 explicitly rules out ("the pages should look the same in
   light theme and merely become correct in dark"). The nesting is preserved
   verbatim and the inks inside it are tokenised in place, so that whoever
   un-nests it in a follow-up gets a correct component rather than a
   light-theme-only one.
   ──────────────────────────────────────────────────────────────────────── */

.coming-soon-wrapper[b-zbw399wple] {
    font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
}

/* Was #f0f7ff, a fixed pale-blue disc that stays pale on the dark card. The
   color-mix keeps the same "faintest possible tint of the brand" intent and
   derives it from --kts-primary and the card, so it lifts in light and
   darkens in dark. */
.icon-circle[b-zbw399wple] {
    background-color: color-mix(in srgb, var(--kts-primary) 8%, var(--kts-card-bg));
    border-radius: 50%;
    width: 5rem;
    height: 5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 0 auto;
}

    /* --kts-slot-text, not --kts-primary: Foundation lightens the primary to
       #2f81f7 in dark, which measures 3.96:1 on this tint. --kts-slot-text is
       the ink Foundation publishes for blue-on-card (--kts-slot-bg is defined
       as the card surface in both themes) and is #0f6cbd in light, identical
       to --kts-primary, so nothing moves in light theme. */
    .icon-circle i[b-zbw399wple] {
        font-size: 2rem;
        /* bg: color-mix(in srgb, var(--kts-primary) 8%, var(--kts-card-bg)) */
        color: var(--kts-slot-text);  /* 4.82 / 7.15 on the tint (was #0078D4: 4.05 / 3.28) */

        .sparkles-icon {
            position: absolute;
            top: -.3125rem;
            right: -.9375rem;
            font-size: 1.25rem;
            color: var(--kts-slot-text);  /* 5.38 / 7.84 on the card (was #0078D4: 4.53 / 3.59) */
        }

        /* --kts-counter-fg is Foundation's ink-on-a-saturated-brand-fill token
           (#ffffff light / #1a1b1e dark). A literal #ffffff would measure
           3.75:1 on --kts-primary in dark and miss AA; the token measures
           5.38:1 light / 4.60:1 dark. */
        .coming-soon-badge[b-zbw399wple] {
            background-color: var(--kts-primary);
            color: var(--kts-counter-fg);
            font-size: .75rem;
            font-weight: 600;
            padding: .5rem 1.25rem;
            border-radius: 3.125rem;
            letter-spacing: .03125rem;
        }

        .message-title[b-zbw399wple] {
            color: var(--kts-heading-color);  /* 16.34 / 14.86 (was #333333: 12.63 / 1.29) */
            font-weight: 400;
            font-size: 1.375rem;
        }

        .message-description[b-zbw399wple] {
            color: var(--kts-text-muted);  /* 4.83 / 6.35 (was #6b7280: 4.83 / 3.37) */
            font-size: .9375rem;
            line-height: 1.6;
            max-width: 37.5rem;
            margin: 0 auto;
        }

        .loading-dots[b-zbw399wple] {
            display: flex;
            justify-content: center;
            gap: .375rem;
        }

            /* The dots carry no information — the "coming soon" message is the
               content — so they are decorative and WCAG sets no contrast floor
               for them. They are still derived from --kts-primary rather than
               left as the fixed #bfdbfe/#60a5fa pair, so they stay visible on
               the dark card instead of glowing pale blue on it. */
            .loading-dots .dot[b-zbw399wple] {
                display: block;
                width: .5rem;
                height: .5rem;
                border-radius: 50%;
                background-color: color-mix(in srgb, var(--kts-primary) 30%, var(--kts-card-bg));
            }

            /* The wave is a decorative, indefinitely-looping animation, so it
               runs only for people who have not asked for less motion; the
               static dots above remain for everyone else. Agenda.razor.css and
               Calendar.razor.css gate their animations the same way. */
            @media (prefers-reduced-motion: no-preference) {
                .loading-dots .dot[b-zbw399wple] {
                    animation: loadingWave-b-zbw399wple 1.5s infinite ease-in-out;
                }
            }

                .loading-dots .dot:nth-child(1)[b-zbw399wple] {
                    animation-delay: 0s;
                }

                .loading-dots .dot:nth-child(2)[b-zbw399wple] {
                    animation-delay: 0.2s;
                }

                .loading-dots .dot:nth-child(3)[b-zbw399wple] {
                    animation-delay: 0.4s;
                }
    }

@keyframes loadingWave-b-zbw399wple {
    0%, 100% {
        background-color: color-mix(in srgb, var(--kts-primary) 30%, var(--kts-card-bg));
        transform: translateY(0);
    }

    50% {
        background-color: color-mix(in srgb, var(--kts-primary) 65%, var(--kts-card-bg));
        transform: translateY(-.25rem);
    }
}
/* /Components/CustomValueComponent.razor.rz.scp.css */
/* Swatch for a Color custom field (#1007).

   Was an inline style carrying `background-color: @Value.ToString()` plus a px
   box and a hardcoded #d1d5db hairline. The fill moved to a resolved token (a
   raw palette key painted nothing); the box is rem so it scales with the user's
   root font size, and the hairline is a Foundation token so it survives the
   theme flip instead of vanishing into a dark card. */
.custom-value-swatch[b-n1xy06vdjm] {
    display: inline-block;
    width: .875rem;
    height: .875rem;
    border-radius: 50%;
    border: .0625rem solid var(--kts-border-subtle);
    margin-right: .25rem;
    flex-shrink: 0;
}
/* /Components/MedicFilterBar.razor.rz.scp.css */
/* The medic medallion bar (#1010, #1119, #1270, #1272), moved here by #1509 when
   the bar became a component shared by Pages/Calendar and Pages/Agenda.

   Scoped to this component on purpose, and the move was FORCED rather than tidy:
   Blazor stamps the owning file's scope attribute onto every selector, so these
   rules left in Pages/Calendar.razor.css would compile to
   `.doctor-avatar-btn[b-<calendar>]` and match nothing on the agenda — an
   unstyled bar, silently. Nothing here is changed from what shipped; the
   declarations are byte-identical, so every contrast figure measured against
   them still describes what ships.

   Lengths are rem for the reason Calendar.razor.css states them in rem (#1018,
   WCAG 1.4.4): a length in px does not answer to the reader's own font size.

   The medic's PICTURE has no rules here. It is rendered by
   ResourceMedallionPicture, a different component, so an `img` selector in this
   file would carry this component's scope id rather than that one's and match
   nothing. Its rules are in Components/ResourceMedallionPicture.razor.css. */

.doctor-avatars-row[b-p6xoy2mwfs] {
    display: flex;
    align-items: center;
    gap: .25rem;
    flex-wrap: wrap;
    justify-content: flex-start;
    padding-bottom: .25rem;
}

/* The medallion's BOX, shared by the live button and by the placeholder the row
   draws while the roster is still loading (#1119).

   Split out of .doctor-avatar-btn rather than duplicated, and that is the whole
   mechanism: the placeholder exists only to hold this row's height, its wrap
   points and its gaps steady from the first frame, and it can only do that if its
   box is identical to the thing it stands in for — same outer size, same border
   width, same flex behaviour. Two rules with the same numbers would drift the
   first time one of them is retuned, and the drift would show up as the grid
   jumping again, which is the defect this row is being changed to fix.

   The border is transparent here and set per medic inline (border-color is the
   medic's identity colour); leaving it transparent on the placeholder keeps the
   2.125rem outer box while showing nothing. */
.doctor-avatar-btn[b-p6xoy2mwfs],
.doctor-avatar-placeholder[b-p6xoy2mwfs] {
    width: 2.125rem;
    height: 2.125rem;
    border-radius: 50%;
    border: .1875rem solid transparent;
    padding: 0;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}

.doctor-avatar-btn[b-p6xoy2mwfs] {
    cursor: pointer;
    overflow: hidden;
    background: none;
}

/* Nothing of its own beyond the shared box: the fill, the pulse and its
   suppression under prefers-reduced-motion are KtsSkeleton's, which the
   placeholder wraps at 100% × 100% — i.e. the content box inside the border,
   which is exactly the medallion's visible circle. Deliberately no local
   background or colour, so there is no second definition of "loading grey" in
   this file to keep in step with Foundation's. */
.doctor-avatar-placeholder[b-p6xoy2mwfs] {
    overflow: hidden;
}

/* The scale/opacity steps below are STATE, not decoration — they are how a
   filtered-out medic reads as filtered out — so they are not gated. Only the
   tween between states is, matching Agenda.razor.css's treatment of its pulse. */
@media (prefers-reduced-motion: no-preference) {
    .doctor-avatar-btn[b-p6xoy2mwfs] {
        transition: opacity .2s ease, transform .2s ease, box-shadow .2s ease;
    }
}

.doctor-avatar-btn.is-selected[b-p6xoy2mwfs] {
    transform: scale(1.08);
}

.doctor-avatar-btn.is-dimmed[b-p6xoy2mwfs] {
    opacity: 0.35;
}

.doctor-avatar-btn:hover[b-p6xoy2mwfs] {
    transform: scale(1.12);
}

/* Sits on the medic's own colour. #1007: that colour is a KtsIdentityPalette
   entry now, and ink in that palette is PER ENTRY — ten of the thirty take
   white, twenty take dark — so a fixed `#fff` here is unreadable on two thirds
   of the palette. Calendar.razor sets --doctor-avatar-ink per medallion from
   KtsIdentityPalette.InkVar (measured, for a legacy hex). The `#fff` survives
   only as the var fallback, matching the ink Foundation puts on its own filled
   chips, for a medallion rendered before the variable is set. */
.doctor-avatar-initials[b-p6xoy2mwfs] {
    font-size: .6875rem;
    font-weight: 700;
    color: var(--doctor-avatar-ink, #fff);
    pointer-events: none;
    user-select: none;
}

/* #1272: a medic WITH a picture draws it here instead of the initials, filling the
   medallion's content box — i.e. the circle INSIDE this button's .1875rem border, which
   is already set to the medic's identity colour per button. That border is the ring the
   request asked for: the picture arrives circled in the medic's colour with no second
   element and no change to this row's box, so the bar's height, wrap points and gaps are
   the same whether a medic has a picture or not — and identical to the loading
   placeholder's, which is the whole point of .doctor-avatar-btn and
   .doctor-avatar-placeholder being one rule (#1119).

   The picture's own rules are NOT here. It is rendered by ResourceMedallionPicture, a
   different component, so an `img` selector in this file would compile to
   `img[b-<medicfilterbar>]` and match nothing — the element carries that component's scope id,
   not this one's. They live in Components/ResourceMedallionPicture.razor.css. */

/* ── Global doctor filter bar ── */
/* Carries the medallions in ALL FOUR views since #1119 — there is no second,
   inline copy in the date header any more. See the note above the bar in
   MedicFilterBar.razor for why the split went away rather than being made consistent,
   and for how the row holds its own space open while the roster loads. */
.global-doctor-filter-bar[b-p6xoy2mwfs] {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: .5rem 1rem;
}

/* ── "Medic type has no colour field" hint (#1010) ── */
/* The explanation that used to be missing when the medallions and the lanes silently
   did not appear. Sized and coloured as a secondary note, not an alert: it is a
   configuration nudge for an admin, not an error, and it is not shown at all to a
   medic viewing their own agenda. */
.medic-colour-hint[b-p6xoy2mwfs] {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .25rem 1rem .5rem;
    margin: 0;
    font-size: .75rem;
    color: var(--kts-text-body);
}
/* /Components/MedicHeaderDetails.razor.rz.scp.css */
/* ────────────────────────────────────────────────────────────────────────────
   Medic/resource header — Foundation-conformant (#1053, band 1 of 3).

   Tokens for ink and chrome, `rem` for every length. Ratios are WCAG 2.1
   relative luminance against `--kts-card-bg` (#ffffff light / #1f2024 dark),
   computed by `tools/css-audit/css_theme_audit.py`.

   `Pages/ResourceDetails.razor.css` carries a near-identical copy of these
   rules, left over from before this header was extracted into a component.
   That copy no longer matches anything (its page renders this component
   instead); this one is the live definition.
   ──────────────────────────────────────────────────────────────────────── */

/* Back link */
.back-link[b-pv9135se75] {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    color: var(--kts-heading-color);  /* 16.34:1 light / 14.86:1 dark (was #1a1a1a: 17.40 / 1.07) */
    font-size: .9375rem;
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    margin-bottom: 1.5rem;
}

/* --kts-primary is the obvious swap for the old Bootstrap #0d6efd, and it is
   the wrong one for TEXT: Foundation lightens it to #2f81f7 in dark, which
   measures 4.34:1 on the card and misses AA. --kts-slot-text is the ink
   Foundation publishes for interactive blue text sitting on --kts-slot-bg,
   and --kts-slot-bg is defined as the card surface in both themes (#ffffff /
   #1f2024), so this is on-label rather than a borrowed token. It is also
   #0f6cbd in light, i.e. exactly --kts-primary, so the hover reads unchanged
   there. Non-text blues (fills, borders, icons) still use --kts-primary. */
.back-link:hover[b-pv9135se75] {
    color: var(--kts-slot-text);  /* 5.38 / 7.84 (was #0d6efd: 4.50 / 3.62) */
}

.back-link i[b-pv9135se75] {
    font-size: .875rem;
}

/* Header card */
.resource-header[b-pv9135se75] {
    margin-bottom: 2rem;
}

.resource-header-top[b-pv9135se75] {
    display: flex;
    align-items: center;
    gap: 1.25rem;
    margin-bottom: 1.25rem;
}

.resource-actions[b-pv9135se75] {
    margin-left: auto;
    flex-shrink: 0;
}

/* The avatar's fill is the medic's identity colour — DATA, not chrome — and
   MedicHeaderDetails.razor sets it inline together with its matching ink
   (`style="background-color: @avatarColor; color: @avatarInk;"`). Both come
   from KtsIdentityPalette via MedicIdentityColor, which is what #1007 landed.
   Neither belongs in this stylesheet and neither is touched here. */
.resource-avatar[b-pv9135se75] {
    width: 4rem;
    height: 4rem;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

/* ── The `i` descendant rules in this file are inert, and this one doubly so ──
   The markup renders `<NavIcon>`, which is a KTS.Foundation component emitting
   an `<svg …stroke="currentColor">`; there is no `<i>` anywhere in the 0.63.0
   assembly. Even if there were, scoped CSS could not reach it: `.resource-avatar
   i` compiles to `.resource-avatar i[b-xxxxx]`, and elements a CHILD component
   renders never carry this component's scope attribute — only `::deep` crosses
   that boundary (see the note at the bottom of Pages/ResourceDetails.razor.css).

   The rule is kept, rather than deleted, because #1053 is a token/unit
   conversion; but the hardcoded `color: #ffffff` it carried is replaced with
   `inherit` rather than with a token, so that IF an `<i>` ever renders here it
   picks up the per-entry identity ink the parent sets inline instead of
   fighting it. That matters because ink in KtsIdentityPalette is per entry —
   twenty of the thirty take dark ink — so a fixed white is unreadable on two
   thirds of the palette in both themes. Calendar.razor.css hit the live version
   of exactly that bug on the medallion initials (#1007).

   Deleting these three inert `i` rules is a good follow-up; it is not this
   change, which would otherwise be unable to claim it only moved colours. */
.resource-avatar i[b-pv9135se75] {
    font-size: 1.75rem;
    color: inherit;
}

.resource-info[b-pv9135se75] {
    flex: 1;
    min-width: 0;
}

.resource-name[b-pv9135se75] {
    font-size: 1.5rem;
    font-weight: 700;
    color: var(--kts-heading-color);  /* 16.34 / 14.86 (was #1a1a1a: 17.40 / 1.07) */
    margin: 0 0 .25rem 0;
}

.resource-subtitle[b-pv9135se75] {
    font-size: 1rem;
    color: var(--kts-text-muted);  /* 4.83 / 6.35 (was #6c757d: 4.69 / 3.47) */
    margin: 0;
}

/* Details row */
.resource-details-row[b-pv9135se75] {
    display: flex;
    align-items: center;
    gap: 1.5rem;
    flex-wrap: wrap;
}

.detail-item[b-pv9135se75] {
    display: flex;
    align-items: center;
    gap: .625rem;
    font-size: .9375rem;
    color: var(--kts-text-body);  /* 10.31 / 10.96 (was #4a4a4a: 8.86 / 1.84) */
}

/* Inert for the same reason as `.resource-avatar i` above — the glyph is a
   NavIcon `<svg>` with no scope attribute from this component, so it actually
   paints at `.detail-item`'s inherited --kts-text-body (10.31 / 10.96), not at
   the value below. The ratio is quoted for the pair the rule DECLARES. */
.detail-item i[b-pv9135se75] {
    color: var(--kts-text-muted);  /* 4.83 / 6.35 (was #6c757d: 4.69 / 3.47) */
    font-size: .9375rem;
    width: 1.125rem;
    text-align: center;
}

/* Also a medic identity colour set inline from MedicIdentityColor — the whole
   point of the swatch is to show which colour this medic owns on the
   calendar, so there is nothing here to tokenise. */
.color-swatch[b-pv9135se75] {
    width: 1.25rem;
    height: 1.25rem;
    border-radius: .25rem;
    flex-shrink: 0;
}
/* /Components/OverrideAppointment.razor.rz.scp.css */
/* Layout only, and every class this file declares is one this component's OWN markup carries.
   That is the rule #1518 exists to enforce: before it, this stylesheet held six .lane-head* rules
   while the markup named twelve other classes, five of which were declared nowhere in the app and
   one of which (.cad-footer) was declared only in ANOTHER component's scoped sheet — where its
   compiled `[b-xxxxx]` selector could never reach this markup. Colours come from Foundation's
   --kts-* tokens so both themes are correct by construction; no hex, no !important.
   Styling/BookingClassResolutionTests fails the build on a class that resolves to nothing. */

.override-body[b-x1tl74vdio] {
    display: flex;
    flex-direction: column;
    gap: var(--book-band-gap);
}

.override-subtitle[b-x1tl74vdio] {
    margin: .25rem 0 0;
    font-size: .75rem;
}

/* The date row is not a KtsFormField: DateNavigator is a CLUSTER of three controls, not one
   input, so there is no single element for a `for` to point at. It gets a group label instead —
   `role="group"` + `aria-labelledby` on the markup — which is the association a composite
   control takes. The caption is styled to read as a peer of KtsFormField's own labels. */
.override-field[b-x1tl74vdio] {
    display: flex;
    flex-direction: column;
    gap: var(--book-cluster-gap);
}

.override-field-label[b-x1tl74vdio] {
    font-size: .8125rem;
    font-weight: 500;
}

/* The board scrolls inside its own box; the modal body must never scroll sideways. The cap is the
   shared --book-pane-height, the same token both panes on the page behind it use — this was
   `height: 650px` on the shimmer and unbounded here, so the modal's board and the page's pane
   disagreed by whatever the viewport happened to be. */
.override-board[b-x1tl74vdio] {
    min-width: 0;
    max-height: var(--book-pane-height);
    overflow: auto;
}

.override-loading[b-x1tl74vdio] {
    display: flex;
    flex-direction: column;
    gap: var(--book-cluster-gap);
    height: var(--book-pane-height);
}

.override-loading-board[b-x1tl74vdio] {
    display: flex;
    gap: var(--book-cluster-gap);
    flex: 1 1 auto;
    min-height: 0;
}

.override-loading-rows[b-x1tl74vdio] {
    display: flex;
    flex-direction: column;
    gap: var(--book-cluster-gap);
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
}

/* Per-doctor lane headers for the TimelineDay booking board (KtsScheduler
   resource lanes). The scheduler grid itself is styled by KTS.Foundation;
   these rules only dress the doctor name + availability chip rendered via
   ResourceHeaderTemplate. Scoped to OverrideAppointment. */

.lane-head[b-x1tl74vdio] {
    display: flex;
    flex-direction: column;
    gap: .125rem;
    padding: .25rem .5rem;
    min-width: 0;
}

.lane-head-name[b-x1tl74vdio] {
    font-size: .75rem;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.lane-head.is-selected .lane-head-name[b-x1tl74vdio] {
    text-decoration: underline;
}

.lane-head-status[b-x1tl74vdio] {
    font-size: .625rem;
    line-height: 1.4;
    padding: 0 .375rem;
    /* Pill sentinel, not a dimension: clamped to half the shorter side, so the unit
       is irrelevant. `999rem` matches Pages/Calendar.razor.css. */
    border-radius: 999rem;
    align-self: flex-start;
    white-space: nowrap;
}

/* The chip's tint is the redundant cue; the WORD inside it carries the meaning (WCAG 1.4.1), which
   is what lets the ink be one token for both states instead of two.

   Both rules used to read `var(--kts-success-strong, #1a7f37)` / `var(--kts-danger-strong,
   #b3261e)`, described as fallbacks "in case the global badge classes are absent". They were not
   fallbacks: KTS.Foundation 0.71.0 declares NEITHER `--kts-success-strong` NOR
   `--kts-danger-strong`, in either theme, so the hex WAS the colour — a light-theme literal
   painting the chip in both themes, which is exactly what a token-driven sheet is for.

   The ink is --kts-text-body rather than --kts-success / --kts-danger because those measure
   4.89 / 4.97 light but 6.27 / 4.06 dark on their own tints — the busy chip lands below the AA
   body floor on 10px text in one theme. --kts-text-body measures 9.39 / 8.44 light and
   8.11 / 9.17 dark on the same two tints. Computed in OverrideModalWiringTests, not assumed. */
.lane-head-status.badge-available[b-x1tl74vdio] {
    background-color: var(--kts-success-soft);
    color: var(--kts-text-body);
}

.lane-head-status.badge-busy[b-x1tl74vdio] {
    background-color: var(--kts-danger-soft);
    color: var(--kts-text-body);
}
/* /Components/PatientData/AppointmentComponent.razor.rz.scp.css */
.appointment-card[b-pu1qwcz2fa] {
    position: relative;
    background: var(--kts-card-bg, var(--kts-body-bg, #fff));
    border: .0625rem solid var(--kts-card-border, var(--kts-border-color, #e5e7eb));
    border-radius: .5rem;
    padding: .75rem 1rem;
    display: flex;
    flex-direction: column;
    gap: .375rem;
}

/* Row 1: date, time, status, button */
.appointment-row-1[b-pu1qwcz2fa] {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.appointment-meta[b-pu1qwcz2fa] {
    display: flex;
    align-items: center;
    gap: .75rem;
    flex-wrap: wrap;
}

.appointment-date[b-pu1qwcz2fa] {
    font-size: .75rem;
    color: var(--kts-body-color, #6b7280);
}

.appointment-time[b-pu1qwcz2fa] {
    font-size: .75rem;
    color: var(--kts-body-color, #6b7280);
}

/* Row 2: service name */
.appointment-service[b-pu1qwcz2fa] {
    font-size: 1rem;
    font-weight: 600;
    color: var(--kts-heading-color, #1f2937);
}

/* Row 3: resources */
.appointment-resources[b-pu1qwcz2fa] {
    font-size: .75rem;
    color: var(--kts-body-color, #6b7280);
}

/* Actions group (Open + Add to cart) */
.appointment-actions[b-pu1qwcz2fa] {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-shrink: 0;
}
/* /Components/PatientData/ConsultationComponent.razor.rz.scp.css */
/* ── Outer wrapper ──────────────────────────────────────────────────────────── */
.consult-wrapper[b-k548afl8k9] {
    display: flex;
    flex-direction: column;
}

.consult-cards[b-k548afl8k9] {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    padding: 1rem .5rem .5rem .5rem;
}

/* ── History / preview sidebar — stays in view while the (usually taller) form
   column on the left scrolls past it. Anchors to the nearest scrolling
   ancestor, which is PatientComponent's .kts-tabs-panel (overflow-y: auto). ── */
.consult-sidebar-sticky[b-k548afl8k9] {
    position: sticky;
    top: 0;
}

/* ── 2-column form grid ─────────────────────────────────────────────────────── */
.consult-form-grid[b-k548afl8k9] {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.125rem 1.75rem;
}

/* 39.9375rem = 639px at the 16px root. In a media query `rem` resolves against the
   BROWSER's root font size, not the page's, so this breakpoint now follows a user's
   font-size preference — a px breakpoint cannot. */
@media (max-width: 39.9375rem) {
    .consult-form-grid[b-k548afl8k9] {
        grid-template-columns: 1fr;
    }
}

/* ── Status toggle — sits in the card header, next to the title ──────────────── */
.consult-status-toggle[b-k548afl8k9] {
    background: none;
    border: none;
    padding: .125rem .375rem;
    margin: -.125rem -.375rem;
    border-radius: .375rem;
    cursor: pointer;
    transition: background-color 0.15s ease, box-shadow 0.15s ease;
}

.consult-status-toggle:not(:disabled):hover[b-k548afl8k9] {
    background-color: rgba(0, 0, 0, 0.06);
}

.consult-status-toggle:not(:disabled):focus-visible[b-k548afl8k9] {
    outline: none;
    box-shadow: 0 0 0 .125rem color-mix(in srgb, var(--kts-primary) 25%, transparent);
}

.consult-status-toggle:disabled[b-k548afl8k9] {
    cursor: default;
    opacity: 0.7;
}

/* ── Load more ──────────────────────────────────────────────────────────────── */
.consult-load-more[b-k548afl8k9] {
    display: flex;
    justify-content: center;
    padding: .75rem;
}

/* ── History table row-edit button — square icon-only variant ────────────────── */
/* KtsButton has no icon-only mode of its own (kts-btn-sm's padding is tuned
   for text+icon), so this squares it up locally rather than adding one to
   the shared library for a single call site. */
[b-k548afl8k9] .consult-edit-row-btn {
    padding: 0.375rem;
}

/* ── The `.consult-field` rules that stood here are gone (#1561) ─────────────────
   There were two: the field's own flex column, and a `.consult-field ::deep …`
   width rule for the controls inside it. Both compiled to `.consult-field[b-xxxxx]
   …` and the class has not been on this component's markup since fe98063b replaced
   the hand-rolled field divs with Foundation's `KtsFormField` — which owns both the
   column and the control width itself. So both rules matched nothing, in either
   direction, and nothing reported it: that is the class of defect #1561's
   `ScopedCssDeepReachTests` now fails a build on.

   `.consult-field ::deep` is worth a second look, because the `::deep` makes it
   look reachable and it is not. `::deep` frees what comes AFTER it; the compound
   BEFORE it is the one that carries the scope attribute, so a `::deep` rule whose
   left-hand side names a class the owner never renders is exactly as dead as one
   without. #1525 is the same shape with a user-visible consequence.
   ──────────────────────────────────────────────────────────────────────────── */
/* /Components/PatientData/PatientComponent.razor.rz.scp.css */
/* NOTE (2026-07): scrolling inside a KtsTabs panel nested in ListLayout's
   Detail slot is a known KTS.Foundation limitation, not specific to this
   page — reproduced on the KTS.Foundation.Demo's own
   Pages/Patterns/TabsDetail.razor by simply growing the panel content, and
   confirmed with Alex as affecting KeyID Client too. Flagged upstream;
   revisit this file once Foundation's ListLayout/KtsTabs height chain is
   fixed. The rules below are the "correct in principle" flex/min-height
   chain — they don't force scrolling today, but are the right shape to
   already be in place when the upstream fix lands. */

.patient-layout[b-9qw3m9jg9d] {
    display: flex;
    height: 100%;
    min-height: 100%;
    max-height: 100%;
    overflow: hidden;
    /* Positioned ancestor for ShoppingCartComponent's CartDrawer
       (Scope="Container") — anchors the drawer to this page instead of the
       viewport, so it doesn't sit behind the AppBar (z-appbar:80 > the
       global cart panel's z-index:61). */
    position: relative;
}

.patient-content[b-9qw3m9jg9d] {
    flex: 1 1 0%;
    min-width: 0;
    min-height: 0;
    height: 100%;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.patient-tab-container[b-9qw3m9jg9d] {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

.patient-header[b-9qw3m9jg9d] {
    flex-shrink: 0;
    /* Right padding clears ListLayout's close FAB (2.75rem square, absolutely
       positioned top-right of the detail column) so header content doesn't
       render underneath / overlap the X. */
    padding: 1rem 4rem 1rem 1.5rem;
}

.patient-header-top[b-9qw3m9jg9d] {
    display: flex;
    align-items: center;
    gap: 1rem;
}

.patient-header-title[b-9qw3m9jg9d] {
    font-size: 1.25rem;
    font-weight: 600;
    color: var(--kts-heading-color, #111827);
    margin: 0;
}

.patient-header-cnp[b-9qw3m9jg9d] {
    font-size: 0.875rem;
    color: var(--kts-body-color, #6b7280);
    margin: 0;
    font-variant-numeric: tabular-nums;
}

[b-9qw3m9jg9d] .patient-tabs {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

[b-9qw3m9jg9d] .patient-tabs .kts-tabs-list {
    flex-shrink: 0;
    background-color: var(--kts-body-bg, #fff);
    padding: 0 1rem;
}

[b-9qw3m9jg9d] .patient-tabs .kts-tabs-panel {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    box-sizing: border-box;
    padding: 1rem;
}
/* /Components/PatientData/PatientPhotoCard.razor.rz.scp.css */
/* #1437, Copilot round 1. These rules live HERE and not in PersonalInfoComponent.razor.css,
   which is where they were written first.

   Blazor CSS isolation rewrites a component's scoped selectors with that component's own
   scope attribute, and the attribute is stamped on the markup the component itself renders —
   never on a child component's internal markup. So the parent's `.patient-photo-row` matched
   nothing once the control moved into its own component, and the heading, the spacing and the
   hint all silently lost their styling. `::deep` would have been the other way to say it, and
   it is the wrong one: the rules belong to this component, not to a descendant selector the
   parent has to remember it owns. */

/* Repeated from the parent card rather than shared: the heading has to sit flush with the
   other section headings on that card, and there is no stylesheet the two components share.
   Kept identical on purpose; PatientPhotoMarkupTests asserts the rule is declared in BOTH
   files, which is the half that matters - a missing one is an unstyled heading. */
.section-heading[b-k95zm3yf3i] {
    font-size: 1rem;
    font-weight: 600;
    color: var(--kts-heading-color, #1f2937);
    margin: 0 0 1rem 0;
    padding-bottom: .5rem;
    border-bottom: .0625rem solid var(--kts-border-color, rgb(237, 235, 233));
}

/* The well brings its own layout (a figure beside its buttons), so this only spaces it from
   the heading above and the fields below. */
.patient-photo-row[b-k95zm3yf3i] {
    margin-bottom: 1.5rem;
}

.patient-photo-hint[b-k95zm3yf3i] {
    color: var(--kts-text-muted, #6b7280);
    font-size: .75rem;
    margin: 0;
}
/* /Components/PatientData/PersonalInfoComponent.razor.rz.scp.css */
/* Main card */
.personal-info-card[b-x4yfn9t0r3] {
    background: var(--kts-body-bg, #fff);
    border: .0625rem solid var(--kts-border-color, #e5e7eb);
    border-radius: .75rem;
    padding: 1.5rem;
}

/* Header */
/* Sticky within .kts-tabs-panel (the scroll container), which has its own
   1rem padding — that padding sits ABOVE this sticky element and never
   scrolls out of view on its own. Negative margin + matching padding pulls
   the header up to cover that gap so no background shows through at top:0. */
/* #1540: the 4rem right padding keeps .header-actions clear of ListLayout's
   floating close × — .list-detail-close-fab (the pinned KTS.Foundation bundle)
   is absolute at top/right:.5rem and 2.75rem square, so its left edge is
   3.25rem in from the pane's right. 4rem = inset + box + .75rem gap; the
   same reservation PatientComponent.razor.css applies to .patient-header. */
.card-header-row[b-x4yfn9t0r3] {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: center;
    gap: .5rem;
    margin: -1rem -1rem 1.5rem -1rem;
    padding: 1rem 4rem .75rem 1rem;
    position: sticky;
    top: -1rem;
    z-index: 5;
    background: var(--kts-body-bg, #fff);
    border-bottom: .0625rem solid var(--kts-border-color, #e5e7eb);
}

.card-main-title[b-x4yfn9t0r3] {
    font-size: 1.125rem;
    font-weight: 600;
    color: var(--kts-heading-color, #1f2937);
    margin: 0;
}

.header-actions[b-x4yfn9t0r3] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
}

/* Form grid - 2 columns */
.form-grid[b-x4yfn9t0r3] {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.5rem;
    margin-bottom: 2rem;
}

.form-field[b-x4yfn9t0r3] {
    display: flex;
    flex-direction: column;
}

.form-field.full-width[b-x4yfn9t0r3] {
    grid-column: span 2;
}

.field-label[b-x4yfn9t0r3] {
    display: block;
    font-size: .875rem;
    font-weight: 600;
    color: var(--kts-body-color, #605e5c);
    margin-bottom: .5rem;
    line-height: 1.3125rem;
}

/* Sections */
.info-section[b-x4yfn9t0r3] {
    margin-bottom: 2rem;
}

.section-heading[b-x4yfn9t0r3] {
    font-size: 1rem;
    font-weight: 600;
    color: var(--kts-heading-color, #1f2937);
    margin: 0 0 1rem 0;
    padding-bottom: .5rem;
    border-bottom: .0625rem solid var(--kts-border-color, rgb(237, 235, 233));
}

/* Emergency contacts - 4 fields + action */
.contact-row[b-x4yfn9t0r3] {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr 1fr auto;
    gap: 1rem;
    align-items: end;
    margin-bottom: .75rem;
}

.contact-field[b-x4yfn9t0r3] {
    display: flex;
    flex-direction: column;
}

.contact-remove[b-x4yfn9t0r3] {
    display: flex;
    align-items: flex-end;
    padding-bottom: .25rem;
}

/* Badges container */
.badges-container[b-x4yfn9t0r3] {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
}

[b-x4yfn9t0r3] .badge-remove {
    cursor: pointer;
    margin-left: .25rem;
    font-size: .75rem;
    opacity: 0.7;
    transition: opacity 0.15s;
}

.badge-remove:hover[b-x4yfn9t0r3] {
    opacity: 1;
}

/* Dropdown row */
.dropdown-row[b-x4yfn9t0r3] {
    display: flex;
    align-items: center;
    gap: .5rem;
    max-width: 31.25rem;
}

.dropdown-input[b-x4yfn9t0r3] {
    flex: 1;
}

/* Allergy details (edit mode) */
.allergy-details-list[b-x4yfn9t0r3] {
    display: flex;
    flex-direction: column;
    gap: .75rem;
    margin-top: 1rem;
}

.allergy-detail-card[b-x4yfn9t0r3] {
    background: var(--kts-surface-sunken, #f9fafb);
    border: .0625rem solid var(--kts-border-subtle, #e5e7eb);
    border-radius: .5rem;
    padding: 1rem;
}

.allergy-detail-header[b-x4yfn9t0r3] {
    margin-bottom: .75rem;
    font-size: .9375rem;
    color: var(--kts-heading-color, #1f2937);
}

.allergy-severity-row[b-x4yfn9t0r3] {
    display: flex;
    align-items: center;
    gap: .5rem;
    font-size: .875rem;
    color: var(--kts-text-muted, #6b7280);
}

/* Notes */
/* #983. The notes box's label is KtsFormField's now, and Foundation paints it `text-gray-700` — a
   literal Tailwind grey rather than a token. (Corrected in PR #1874: it was believed to have no
   dark-theme variant; the theme stylesheet overrides it under [data-theme=dark] with !important,
   which beats this rule, so this rule sets the light-theme ink only.) ::deep is the only way a scoped sheet reaches a child component's own markup, and
   .notes-field keeps the fix on THIS field rather than on the file's other KtsFormFields, whose ink
   is the app-wide Foundation question and not #983's. The fallback matches .field-label above, which
   is the ink this label had before the move.

   .notes-field is on the <div> the markup writes around the field, NOT on KtsFormField: Blazor
   stamps the scope attribute on the last compound before ::deep and only on markup authored here, so
   a class passed to the child would be scoped to Foundation and this rule would match nothing
   (Copilot, review round 1 — ScopedCssDeepReachTests models it now). */
.notes-field[b-x4yfn9t0r3]  .kts-field-label {
    color: var(--kts-body-color, #605e5c);
}

/* Empty text */
.empty-text[b-x4yfn9t0r3] {
    font-size: .875rem;
    color: var(--kts-text-muted, #6b7280);
    font-style: italic;
    margin: 0;
}

/* /Components/PatientData/ScanCIComponent.razor.rz.scp.css */
/* ────────────────────────────────────────────────────────────────────────────
   CI scan — camera modal and confirm dialog — Foundation-conformant
   (#1053, band 1 of 3).

   Tokens for ink and chrome, `rem` for every length. Ratios are WCAG 2.1
   relative luminance against the surface each rule actually paints on — the
   modal body is `--kts-card-bg` (#ffffff light / #1f2024 dark) — computed by
   `tools/css-audit/css_theme_audit.py`.

   ── THE ONE BARE HEX LEFT IN BAND 1, AND WHY ──
   `.ci-video-container` keeps `background: #000`. It is the letterbox behind a
   live camera feed and the captured still, i.e. media presentation rather than
   chrome, and it must NOT flip with the theme: a light letterbox behind a
   camera preview is wrong in light theme too, and the two inks painted on it
   are chosen against black in both themes. Foundation publishes no media or
   viewport-surface token to point at, and inventing a `var(--kts-…, #000)`
   that resolves to nothing would only hide the literal from a counter rather
   than remove it. The same latitude #1053 grants appointment-status and medic
   identity colours applies: it is kept, and this is the reason.
   ──────────────────────────────────────────────────────────────────────── */

.ci-action-buttons[b-2dl0nodn6x] {
    display: flex;
    gap: .5rem;
}

/* ── Camera modal ── */
.ci-camera-content[b-2dl0nodn6x] {
    display: flex;
    flex-direction: column;
    gap: .75rem;
}

.ci-camera-select-row[b-2dl0nodn6x] {
    display: flex;
    align-items: center;
    gap: .5rem;
}

/* Dead until #983's guard tightening caught it, and this is the shape it was dead in: the class is
   passed to KtsSelect, so the element carrying it is rendered by Foundation and can never take THIS
   sheet's scope attribute. Anchored on the row instead — written in ScanCIComponent.razor, so the
   select is a descendant the rule reaches. The intent is unchanged: the select takes the row's
   slack, beside the row's gap. */
.ci-camera-select-row[b-2dl0nodn6x]  .ci-camera-select {
    flex: 1;
}

.ci-video-container[b-2dl0nodn6x] {
    position: relative;
    width: 100%;
    background: #000;  /* see the header note — media letterbox, deliberately theme-fixed */
    border-radius: .375rem;
    overflow: hidden;
    aspect-ratio: 16 / 9;
    display: flex;
    align-items: center;
    justify-content: center;
}

.ci-video[b-2dl0nodn6x],
.ci-captured-img[b-2dl0nodn6x] {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

.ci-hidden[b-2dl0nodn6x] {
    display: none !important;
}

/* The two rules below sit on that fixed black letterbox, so their ink has to
   be fixed light in both themes — a theme-flipping token would go dark against
   black in light theme. --kts-identity-ink-light is Foundation's published
   light-ink constant (#ffffff, not remapped in dark), stepped down with
   color-mix to keep the placeholder recessive rather than shouting. */
.ci-camera-placeholder[b-2dl0nodn6x] {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .75rem;
    /* bg: #000 */
    color: color-mix(in srgb, var(--kts-identity-ink-light) 70%, transparent);  /* 9.96:1 on #000 in both themes (was #aaa: 9.04) */
    font-size: .875rem;
    position: absolute;
    inset: 0;
}

/* Was #555 on black — 2.82:1, which fails even the 3:1 floor WCAG 1.4.11 sets
   for meaningful non-text content, in every theme. */
.ci-camera-icon[b-2dl0nodn6x] {
    font-size: 3rem;
    /* bg: #000 */
    color: color-mix(in srgb, var(--kts-identity-ink-light) 55%, transparent);  /* 6.27:1 on #000 (was #555: 2.82) */
}

/* ── Confirm dialog ── */
.ci-dialog-content[b-2dl0nodn6x] {
    display: flex;
    gap: 1.5rem;
}

.ci-image-panel[b-2dl0nodn6x] {
    flex: 0 0 20rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .5rem;
}

.ci-image-panel[b-2dl0nodn6x] {
    overflow: hidden;
    border-radius: .375rem;
    /* .0625rem is exactly 1px at the default 16px root and borders snap to
       whole device pixels, so the hairline stays crisp while scaling with the
       user's root font size (WCAG 1.4.4). */
    border: .0625rem solid var(--kts-border-subtle);
    user-select: none;
}

.ci-preview-img[b-2dl0nodn6x] {
    width: 100%;
    object-fit: contain;
    max-height: 26.25rem;
    display: block;
    transform-origin: center center;
    transition: transform 0.05s ease-out;
    will-change: transform;
}

.ci-doc-number[b-2dl0nodn6x] {
    font-size: .8125rem;
    color: var(--kts-text-muted);  /* 4.83:1 light / 6.35:1 dark (was #555: 7.46 / 2.18) */
    margin: 0;
}

.ci-fields-panel[b-2dl0nodn6x] {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: .625rem;
}

.ci-field-row[b-2dl0nodn6x] {
    display: flex;
    flex-direction: column;
    gap: .25rem;
}

.ci-label[b-2dl0nodn6x] {
    font-size: .75rem;
    font-weight: 600;
    color: var(--kts-text-muted);  /* 4.83 / 6.35 (was #666: 5.74 / 2.83) */
    text-transform: uppercase;
    letter-spacing: .025rem;
}

.ci-confidence-banner[b-2dl0nodn6x] {
    display: flex;
    align-items: flex-start;
    gap: .5rem;
    padding: .625rem .875rem;
    border-radius: .375rem;
    font-size: .8125rem;
    line-height: 1.4;
    margin-bottom: .25rem;
}

.ci-confidence-banner i[b-2dl0nodn6x] {
    margin-top: .0625rem;
    flex-shrink: 0;
}

/* ── Status banners ──
   These are chrome, not data: they report how confident the document-
   intelligence extraction was, so the colour is a severity signal Foundation
   already owns. The shape follows Foundation's own dark-theme callout rules
   (`[data-theme=dark] .kts-callout-warning`, `… .kts-callout-danger`): a ~10%
   status wash for the fill and the status token for the icon. The difference
   is that Foundation can write one set of literals for light and another in
   its theme stylesheet, whereas a scoped stylesheet has to say it once — so
   every value here is derived from the status token and the card, which
   already flip.

   The body ink is `color-mix(status, --kts-text-strong)` rather than the flat
   --kts-text-body so the banner keeps the tinted, "reads as a warning" ink the
   old #7a5c00/#7f1715 pair had, in both themes instead of only in light. */
.ci-confidence-warning[b-2dl0nodn6x] {
    background: color-mix(in srgb, var(--kts-warning) 12%, var(--kts-card-bg));
    border: .0625rem solid var(--kts-warning);
    color: color-mix(in srgb, var(--kts-warning) 50%, var(--kts-text-strong));  /* 5.51:1 light / 8.12:1 dark on the fill (was #7a5c00 on a fill that never darkened: 5.89 in both) */
}

/* --kts-warning-text, not --kts-warning, for the icon: the plain warning token
   measures 1.96:1 on its own light fill, below even the 3:1 non-text floor —
   the old #f9a825 on #fff8e1 was 1.85:1 and just as unreadable. */
.ci-confidence-warning i[b-2dl0nodn6x] {
    /* bg: color-mix(in srgb, var(--kts-warning) 12%, var(--kts-card-bg)) */
    color: var(--kts-warning-text);  /* 4.58 / 5.29 on the fill (was #f9a825: 1.85) */
}

.ci-confidence-error[b-2dl0nodn6x] {
    background: color-mix(in srgb, var(--kts-danger) 12%, var(--kts-card-bg));
    border: .0625rem solid var(--kts-danger);
    color: color-mix(in srgb, var(--kts-danger) 50%, var(--kts-text-strong));  /* 9.74:1 light / 7.17:1 dark on the fill */
}

/* Foundation sets a bare `var(--kts-danger)` on its own dark callout icon, and
   copying that verbatim would leave this glyph at 4.91:1 light / 4.23:1 dark on
   the fill above. A glyph is non-text, so WCAG 1.4.11 asks only 3:1 of it and
   4.23 would pass — but there is no reason to ship a number under the bar when
   a token-derived one clears it, and --kts-warning-text does exactly this job
   on the warning side. There is no --kts-danger-text to match it, so the ink is
   deepened towards --kts-text-strong, which darkens it in light and lightens it
   in dark while keeping it unmistakably red. */
.ci-confidence-error i[b-2dl0nodn6x] {
    /* bg: color-mix(in srgb, var(--kts-danger) 12%, var(--kts-card-bg)) */
    color: color-mix(in srgb, var(--kts-danger) 75%, var(--kts-text-strong));  /* 6.98 / 5.38 on the fill (was #e53935: 3.70) */
}
/* /Components/ResourceMedallionPicture.razor.rz.scp.css */
/*
 * #1272 — a resource's profile picture, filling whatever medallion box its caller drew.
 *
 * These rules are HERE and not in the caller's stylesheet because Blazor's CSS isolation
 * appends a per-component scope attribute to every element authored in that component's
 * markup: the `img` below is authored in ResourceMedallionPicture.razor, so a rule written
 * in Pages/Calendar.razor.css would compile to `.resource-medallion-picture[b-<calendar>]`
 * and match nothing at all. The picture would render unsized and unclipped — a full-width
 * rectangle where a 2.125rem circle belongs — and nothing would fail to build.
 *
 * 100%/100% rather than a fixed size, because the box belongs to the caller: the calendar's
 * filter bar sizes it through .doctor-avatar-btn, whose numbers are shared with the loading
 * placeholder so the row cannot change height when the roster resolves (#1119).
 *
 * object-fit, because a portrait is never square and the alternative is a squashed face.
 * border-radius as well as the caller's own overflow clip: a raster child painting past a
 * rounded overflow boundary is a long-standing rendering difference between browsers, and a
 * medallion that shows as a square in one of them is not worth leaving to it.
 */
.resource-medallion-picture[b-6yz8j2ctjp] {
    width: 100%;
    height: 100%;
    border-radius: 50%;
    object-fit: cover;
    display: block;
    pointer-events: none;
    user-select: none;
}
/* /Components/Scheduling/AgendaOverrideNote.razor.rz.scp.css */
/* ────────────────────────────────────────────────────────────────────────────
   OVERRIDE MARKING (#1266) — the row's "somebody tapped 'book anyway'" chip.

   MOVED HERE FROM Pages/Agenda.razor.css WITH ITS MARKUP (#1456 review round 8),
   and the move was not optional once the markup moved. Blazor CSS isolation
   stamps each COMPONENT's own scope attribute onto the elements that component
   authors, so `.agenda-override` rules left behind in the page's stylesheet
   would have been rewritten to `.agenda-override[b-<page>]` and matched nothing
   at all — the chip would have lost every rule below while the build stayed
   green and every source-reading test kept passing. A stylesheet follows its
   markup; it does not point at it.

   NO ::deep on these rules, and that is the same distinction the page's
   stylesheet drew, only from the other side. The markup below is authored in
   AgendaOverrideNote.razor itself, so the Razor compiler stamps THIS
   component's scope attribute onto these very elements and a plain class
   selector matches. The one exception is the icon's <svg>, which NavIcon
   renders — hence the single `::deep` on that rule and nowhere else.

   DISTINCT FROM THE CHECKOUT WASH BY KIND, not just by hue. The wash is a
   background on the whole <tr> and stays in Pages/Agenda.razor.css, where the
   row is; this is a bordered chip inside one cell, so a row that is both
   checked-out and force-booked shows both. Two washes would have been one wash.

   COLOUR IS NOT THE ONLY SIGNAL: a triangle-alert icon, a leading edge, and a
   written label carry it as well.

   CONTRAST, measured (AgendaOverrideMarkerContrastTests computes every number
   from the pinned Foundation palette, the inks out of THIS file and the wash
   fraction out of Pages/Agenda.razor.css — the two halves now live in two
   files, and the suite reads each from where it is, so a change to either is
   re-measured rather than going stale):

     ink                        light row   light wash   dark row   dark wash
     --kts-risk-high-text        5.18:1      4.51:1       7.19:1     5.27:1
     --kts-text-body            10.31:1      8.98:1      10.96:1     8.03:1

   The wash column is the worst case — the checked-out row, where this chip has
   the least headroom. All four ink/theme pairs clear WCAG AA body text (4.5:1),
   and the icon clears 1.4.11 non-text (3:1) with room to spare.

   THE LIGHT WASH COLUMN HAS 0.01 OF HEADROOM, and that is not a rounding
   artefact. The wash column was 4.80:1 over the 10% tint this marker was built
   against; #1265 then raised the checked-out tint to 18% and the same ink over
   the same card measures 4.51:1. It still clears AA, and the suite RE-MEASURES
   it rather than quoting these numbers, so the next person to deepen that tint
   gets a red test instead of an illegible chip — which is the intended
   failure, not a surprise. Deepening it at all now means repainting this chip
   (a darker --kts-risk-high-text, or lifting the label to --kts-text-body and
   leaving the hue to the icon and the edge).
   ──────────────────────────────────────────────────────────────────────── */
.agenda-override[b-q708di6bcq] {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.25rem;
    margin-top: 0.125rem;
    padding-left: 0.375rem;
    border-left: 0.125rem solid var(--kts-risk-high-text);
    font-size: 0.75rem;
    line-height: 1.25;
    color: var(--kts-text-body);
    /* A reason is free text with no length limit and no guaranteed spaces in
       it. `anywhere` — not `break-word` — is what keeps a single unbroken
       400-character token from widening the table and giving the whole page a
       horizontal scrollbar: only `anywhere` reduces the box's MIN-CONTENT
       width, which is the figure table layout sizes this column from. It is
       inherited, so the summary and the full text below get it too.

       `min-width: 0` and `max-width: 100%` used to sit here with that same
       sentence attached. Both were inert and are gone: this box is a flex
       CONTAINER, not a flex item — its parent is the plain <div> wrapper of the
       patient CellTemplate — so `min-width`'s initial value is already 0
       (`auto` applies to flex and grid items only), and `max-width: 100%` on a
       block box that already fills its containing block does nothing. The one
       place that rationale is true is `.agenda-override-summary` below, which
       IS a flex item of this container. */
    overflow-wrap: anywhere;
}

/* The chip is focusable so the full reason is reachable without a mouse; a
   focusable thing with no visible focus ring is reachable in name only. */
.agenda-override:focus-visible[b-q708di6bcq] {
    outline: 0.125rem solid var(--kts-primary);
    outline-offset: 0.125rem;
    border-radius: 0.125rem;
}

.agenda-override-icon[b-q708di6bcq] {
    display: inline-flex;
    align-self: center;
    color: var(--kts-risk-high-text);
}

/* NavIcon renders the <svg>, so this one selector has to reach out of the
   page's scope — see the block header. */
.agenda-override-icon[b-q708di6bcq]  svg {
    width: 0.875rem;
    height: 0.875rem;
}

/* No `white-space: nowrap` here, deliberately. It was here to keep the label on
   one line, but a nowrap label is an unbreakable min-content contribution, and
   `overflow-wrap: anywhere` above cannot touch it — so on a narrow patient
   column it was the LABEL, not the reason, that set the column's floor. The
   chip is `flex-wrap: wrap`, which already gives the label its own line before
   it has to break inside a word. */
.agenda-override-label[b-q708di6bcq] {
    color: var(--kts-risk-high-text);
    font-weight: 600;
}

/* #1312 — WHICH rules this booking is breaking right now, one chip each.
   Rendered only for a LIVE override; a resolved or pre-deployment one has no
   categories and this box is not in the markup at all, which is why nothing
   above needed changing.

   NO NEW COLOUR. The border is `currentColor` and the text is inherited, so
   these chips paint themselves from `.agenda-override`'s own
   `--kts-text-body` on the same surfaces AgendaOverrideMarkerContrastTests
   already measures — deliberately, so this issue does not open a second
   contrast surface that has to be re-measured in both themes. The urgency is
   carried by the icon, the left edge and the label, which are the parts that
   were already --kts-risk-high-text; the chips are structure, not alarm, and
   four alarm-coloured chips on one row would read as four separate warnings.

   `flex-wrap: wrap` on the container means these get their own line before the
   reason has to break inside a word, exactly as the label does. */
.agenda-override-chips[b-q708di6bcq] {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    min-width: 0;
}

.agenda-override-chip[b-q708di6bcq] {
    padding: 0 0.25rem;
    border: 0.0625rem solid currentColor;
    border-radius: 0.125rem;
    /* Chips name a closed vocabulary, so they are short by construction and a
       break inside one would be a break inside a translated phrase — the one
       place on this marker where `overflow-wrap: anywhere` (inherited from the
       container, for free text) is the wrong behaviour. */
    overflow-wrap: normal;
    /* No `opacity` and no lightened colour, on purpose: either would silently
       reduce the measured ratio of text this suite has already signed off, and
       an opacity-thinned chip is exactly the kind of change a contrast test
       reading declared colours cannot see. */
}

.agenda-override-summary[b-q708di6bcq] {
    min-width: 0;
}

/* The FULL reason, always in the accessibility tree and off-screen until asked
   for. Visually-hidden rather than `display: none`, which would take it out of
   the accessibility tree too and leave a screen reader with the truncated
   summary — the one copy that is aria-hidden. */
.agenda-override-full[b-q708di6bcq] {
    position: absolute;
    width: 0.0625rem;
    height: 0.0625rem;
    margin: -0.0625rem;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* On demand — hover OR keyboard focus, which is the acceptance criterion. The
   summary steps aside as the full text appears, so the reason is never printed
   twice. */
.agenda-override:hover .agenda-override-summary[b-q708di6bcq],
.agenda-override:focus-within .agenda-override-summary[b-q708di6bcq] {
    display: none;
}

.agenda-override:hover .agenda-override-full[b-q708di6bcq],
.agenda-override:focus-within .agenda-override-full[b-q708di6bcq] {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip-path: none;
    white-space: normal;
    min-width: 0;
    overflow-wrap: anywhere;
}

/* #1443: WHO force-booked this and WHEN, on its own line under the reason.

   `:not(:empty)` carries the common case rather than a second `@if` in the
   markup. Every appointment booked before #1312 has a reason and no stamps —
   there was no backfill — so the span is emitted with nothing in it on most
   override rows, and an unconditional `display: block; margin-top` would have
   opened a gap under the reason on exactly those rows. Empty, it stays an
   inline box of zero width with no margin and the row reads as it does today;
   non-empty, it becomes the line below.

   NO COLOUR AND NO OPACITY. It inherits `.agenda-override`'s
   `var(--kts-text-body)`, so the ratios AgendaOverrideMarkerContrastTests
   measured for the reason describe this line too. A lightened "metadata grey"
   or an `opacity` here would be a second contrast surface a stylesheet-reading
   suite cannot measure, which is the #1008/#1053 defect. Italic is the whole of
   the distinction: it separates the system's own record from the receptionist's
   typed words without introducing an ink. */
.agenda-override-attribution:not(:empty)[b-q708di6bcq] {
    display: block;
    margin-top: 0.125rem;
    font-style: italic;
}

/* THE REVEAL IS IN FLOW, SO IT IS BOUNDED. Swapping a 60-element summary for a
   400-character reason grows this row and pushes every row below it down;
   leaving the chip snaps them back. On a screen whose last column carries
   status actions, an unbounded version of that is a pointer aimed at row 4's
   button while rows are shifted, and a click that lands on row 5. Capping the
   chip at four lines (0.9375rem each at 0.75rem/1.25) bounds the movement to
   three lines whatever the reason's length, and the rest of the reason is
   still readable by scrolling.

   THE SCROLLER IS THE CHIP, not the text inside it, and that is the whole
   reason this rule is on `.agenda-override` rather than on
   `.agenda-override-full`. The chip is the element that carries tabindex="0",
   so a keyboard user who has focused it can scroll it with the arrow keys, and
   axe's scrollable-region-focusable (WCAG 2.1.1) is satisfied by the attribute
   that is already there. A max-height on the inner span would have made the
   overflow unreachable without a mouse — which is the one thing this marker
   exists to avoid.

   NOT a popover, and not for want of trying: KTS.Foundation's DataTable wraps
   its <table> in `overflow-x: auto` (Components/DataTable.razor, "The
   horizontal-scroll wrapper IS the sticky header's scrollport"), and per CSS
   Overflow 3 that makes the wrapper a scroll container in BOTH axes — the
   package's own comment says as much. An absolutely-positioned reveal would
   therefore be clipped by it on the rows nearest the bottom, which is worse
   than a bounded reflow because the text simply is not there. Escaping the
   scrollport needs the top layer (the HTML `popover` attribute) plus anchor
   positioning, which is a design change and needs a browser to judge — #1312. */
.agenda-override:hover[b-q708di6bcq],
.agenda-override:focus-within[b-q708di6bcq] {
    max-height: 3.75rem;
    overflow-y: auto;
}

/* /Components/ShoppingCartComponent.razor.rz.scp.css */
/* ────────────────────────────────────────────────────────────────────────────
   Shopping-cart picker dialogs — Foundation-conformant (#1053, band 1 of 3).

   Tokens for ink, `rem` for every length. Ratios are WCAG 2.1 relative
   luminance against `--kts-card-bg` (#ffffff light / #1f2024 dark), the
   surface Foundation's modal body paints, computed by
   `tools/css-audit/css_theme_audit.py`.
   ──────────────────────────────────────────────────────────────────────── */

.picker-search[b-j5creowz4q] {
    padding: .75rem 1rem;
}

.picker-list[b-j5creowz4q] {
    max-height: 23.75rem;
    overflow-y: auto;
    padding: .5rem 1rem;
}

/* Prepaid Warning Dialog */
.prepaid-warning[b-j5creowz4q] {
    display: flex;
    flex-direction: column;
    gap: .5rem;
    padding: .5rem 0;
}

/* --kts-warning-text, not --kts-warning: the plain warning token is the fill
   amber (#f59e0b light / #d29922 dark) and measures 2.15:1 on the light card,
   while --kts-warning-text is the readable pair Foundation ships for exactly
   this job (#b45309 / #d29922) at 5.02:1 / 6.45:1. The old #d97706 measured
   3.19:1 light — under AA on the theme it was actually designed for. */
.prepaid-warning-icon[b-j5creowz4q] {
    color: var(--kts-warning-text);
    font-size: 1.5rem;
    margin-bottom: .25rem;
}

/* --kts-text-body is #374151 in light, i.e. the exact value this rule already
   carried, so nothing moves in light theme; in dark it becomes #cbd5e1
   instead of staying a near-black 1.58:1. */
.prepaid-warning p[b-j5creowz4q] {
    margin: 0;
    font-size: 0.9rem;
    color: var(--kts-text-body);  /* 10.31:1 light / 10.96:1 dark (was #374151: 10.31 / 1.58) */
    line-height: 1.5;
}
/* /Layout/MainLayout.razor.rz.scp.css */
/* /Pages/Agenda.razor.rz.scp.css */
/* ────────────────────────────────────────────────────────────────────────────
   CHECKED-OUT rows — the attention highlight the pre-Foundation agenda carried,
   restored on Foundation's DataTable `RowClass` hook (kts.foundation#279), and
   reachable since #1265.

   These rules shipped dead. The predicate that applied the class was
   `a.Status == "checkout" && a.AmountDue > 0`, and AmountDue was hardcoded to 0
   wherever a row was built, so nothing below ever matched anything. #1265 deleted
   AmountDue and the predicate now lives in Helpers/AgendaRowHighlight, where a
   test can execute it. The rule is `checkout`, full stop: narrowing it back to
   "checked out and NOT yet paid" needs a payment signal the system does not model
   (#1277).

   ::deep is REQUIRED here. The <tr> is rendered by the DataTable component,
   so it never carries this page's scope attribute; the wrapper <div> around
   the table does. A plain `tr.row-pulse-attention` in this file compiles to
   `tr.row-pulse-attention[b-xxxxx]` and silently matches nothing —
   `::deep tr.row-pulse-attention` compiles to `[b-xxxxx] tr.row-pulse-attention`
   and reaches through. (Same requirement the reviewed reference in
   Foundation's own DataTableShowcase documents.)

   No `:hover` co-selector is needed as of KTS.Foundation 0.59.2/0.60.0
   (kts.foundation#298 → #301): DataTable's row-hover fill moved to a
   zero-specificity `:where(.kts-dt-row:hover)` rule, so this plain class
   out-specifies it and the tint survives hover.

   The wash is derived from --kts-warning rather than a fixed amber so it
   tracks the theme — dark mode remaps both that token (#f59e0b → #d29922)
   and the cell text colours, and a light literal would be unreadable there.

   NO PULSE — deliberately, and this is a REVERSAL (#1265, spec corrected
   2026-08-24). The rules used to carry a 10% → 3% fade behind a
   `prefers-reduced-motion: no-preference` gate. It is deleted, because `checkout`
   is TERMINAL on this page: Helpers/AppointmentStatusTransitions returns no
   onward transition from it, the actions cell offers only the pay link, and
   HandleAction(id, "payment") navigates without writing a status. So the
   highlighted set only ever grows — most of a 20-row page by late afternoon, and
   every row of any past date, animating forever for a visit that is closed. The
   gate covered only viewers who set the OS preference; there was no in-page way
   to stop it (WCAG 2.2.2, Level A). It also fought its own purpose: the lit end
   of the fade measured 1.02:1 against the card in light, so for half of every
   cycle it was less visible than the tint it interrupted. The class name is kept as
   `row-pulse-attention` because it is the name the pre-Foundation stylesheet used
   and renaming it buys nothing.

   DEPTH IS 18%, up from the 10% that shipped with the dead rules. 18% is the
   depth Foundation's own DataTableShowcase uses for a highlighted row — an
   existing in-house precedent rather than a number chosen here. The 10% was
   picked because the row's UNCORRECTED inks fall under AA above it; the two
   correction rules below are what remove that constraint, so with them in place
   the demo depth is available and is taken.

   Uncorrected, this row's two secondary inks have almost no headroom on the plain
   card: `.text-muted` (--kts-text-muted) measures 4.83:1 there and the pay link's
   `.text-green-700` 4.9:1, and on the 18% wash they fall to 4.21:1 and 4.3:1 —
   both under AA. Corrected, every ink on the row clears it:

     ink on the wash                          light      dark
     · patient name, .text-strong             15.5:1     10.9:1
     · motive, corrected to --kts-text-body    9.0:1      8.0:1
     · ringed status badge, the palette's       6.70:1     7.82:1
     · pay link, corrected to --kts-success    4.68:1     8.5:1
     · the wash itself, against --kts-card-bg  1.15:1     1.37:1

   The badge is on that list because `checkout` is the only status this page
   RINGS — a border in the status colour over a transparent centre
   (Styles/_agenda-status-badge.scss) — so the wash is its background, on exactly
   the highlighted rows and nowhere else. Every other status is a tinted badge
   whose own background sits between its ink and the wash, and is measured in
   AgendaStatusBadgeContrastTests instead.

   THAT INK CHANGED IN #1326, which is why the light figure moved from 4.78:1.
   The ring used to be Foundation's outlined-success ink (#047857 light, #6ee7b7
   dark) because the badge was rendered `Outlined` with BadgeVariant.Success; it
   is now --simplinica-status-checkout (#065f46 light, #6ee7b7 dark), the same
   value the check-out ACTION button is filled with. The dark COLOUR is unchanged —
   the two palettes happen to agree on that one value — though the figure is now
   recorded to two decimals rather than one, because a plain hex supports the
   second digit where the oklch-derived greens below do not. The light figure
   improves, because the palette's green is a shade deeper than Foundation's.

   FLOOR, asserted: the wash must measure at least 1.14:1 in light and 1.36:1 in
   dark against --kts-card-bg. Those are the two figures in the last row of the
   table truncated to two decimals, so the guard IS the measurement rather than a
   rounder number picked beside it, and a shallower wash cannot satisfy it. It is
   a drift guard and NOT a perceptibility result — see below for what this axis
   can actually reach.

   The ceiling is the pay link, not taste. At 23% it measures 4.50:1 and at 24%
   4.47:1 — under AA — so 23% is the deepest wash this row's own text permits.
   18% is shipped rather than 23% because it leaves the binding ink 0.18 above the
   AA floor instead of 0.0007, and the 23% wash buys only 1.19:1 against the card
   in exchange. Deepening the corrections does not move the wall far either: the
   next ink up is the ringed badge, which since #1326 does not bind until about
   70% (the palette's green is deeper than Foundation's outlined-success ink was,
   which used to cap it around 26%).

   PERCEPTIBILITY, STATED RATHER THAN IMPLIED. 1.15:1 is not a perceptibility
   result and neither is the 1.19:1 ceiling. WCAG 1.4.11 asks 3:1 of meaningful
   non-text content, and against --kts-card-bg that is UNREACHABLE AT ANY DEPTH:
   a solid, undiluted --kts-warning fill is 2.15:1 in light. So the wash is a
   SUPPORTING cue and cannot be made into the signal by raising it further. What
   identifies the row is the ringed check-out badge and the "click to pay" link,
   which is why this is not a WCAG 1.4.1 failure. Carrying the signal on a second
   channel — an inset accent bar in --kts-warning-text, which is 5.02:1 against
   the card and is how Foundation marks a selected row — would clear 3:1, but it
   is a visual-design decision for a human rather than something #1265 authorises.
   Raised in the PR for #1265; do not implement it here on your own judgement.

   Every figure above is RE-DERIVED BY A TEST from KTS.Foundation 0.68.0 and the
   shipped Tailwind v4 build, never retyped:
   Tests/Admin.Web.Tests/Scheduling/AgendaAttentionRowContrastTests reads these
   numbers, the depth, and the two corrected ink tokens out of this very file and
   fails if the arithmetic over the pinned palette disagrees at the precision each
   is written to. The two green-700 figures are written to ONE decimal on purpose:
   Tailwind v4's `oklch(52.7% .154 150.069)` is marginally outside sRGB, the
   converter clips per channel where CSS Color 4 §13 specifies chroma reduction,
   and engines differ — the second decimal is not a digit the method can support.
   ──────────────────────────────────────────────────────────────────────── */
[b-o4y2311xnc] tr.row-pulse-attention {
    background-color: color-mix(in srgb, var(--kts-warning) 18%, transparent);
}

/* Motive / notes sub-text: --kts-text-muted has too little headroom to sit on
   the wash, --kts-text-body has plenty, and both tokens are theme-aware. */
[b-o4y2311xnc] tr.row-pulse-attention .text-muted {
    color: var(--kts-text-body);
}

/* Same for the "click to pay" link in the actions cell — the .text-green-700 user
   this rule now corrects. (#1265 deleted the outstanding-amount cell it was
   written for, whose green was applied by the same class; the pay link sits in
   every checked-out row, so it is on the tint on every highlighted row rather
   than on none.) --kts-success is a shade darker than Tailwind's green-700 and is
   what keeps the link at AA on an 18% wash. Light mode only in practice —
   Foundation's dark theme remaps .text-green-700 with !important to a light mint
   (color-mix(--kts-success 50%, white) = #9ae9cc) that measures 8.5:1 over the
   wash on its own. */
[b-o4y2311xnc] tr.row-pulse-attention .text-green-700 {
    color: var(--kts-success);
}

/* ────────────────────────────────────────────────────────────────────────────
   ROW DENSITY — the interim half of #1261.

   The agenda table is the receptionist's whole working surface, and every row
   of padding is one fewer patient on screen. Foundation's DataTable hardcodes
   `px-4 py-3` on every cell, which is 12 px above and 12 px below before the
   content asks for anything.

   THIS IS AN OVERRIDE, AND IT IS INTERIM. Row density is cross-cutting UI
   infrastructure and belongs upstream: KTS.Foundation 0.68.0's DataTable
   exposes no density parameter (the whole reflected list is Items, Columns,
   PageSize, PageSizeChanged, PageSizes, ShowPagination, ItemKey, RowClass,
   IsLoading, Live, Class, MaxHeight, ExternalScrollContainer,
   AdditionalAttributes, EmptyTemplate, SelectedItem, SelectedItemChanged,
   OnRowActivated, ItemsProvider, Sort, SortChanged, FilterText,
   FilterTextChanged, ColumnFilters, ColumnFiltersChanged — `Dense` is on
   KtsStatCard, which is why the page already passes it there, and is not on
   DataTable). Asked for as kts.foundation#437. When that lands this block goes
   and Pages/Agenda.razor passes the parameter instead. It is deliberately ONE
   block naming that issue rather than rules scattered through this file.

   ::deep is REQUIRED, for the same reason the highlight above documents: the
   <tr> and <td> are rendered by DataTable, so they never carry this page's
   scope attribute. The wrapper <div> around the table does.

   IT DOES NOT FIGHT THE HOVER FILL. DataTable's row hover is a zero-specificity
   `:where(.kts-dt-row:hover)` rule since kts.foundation#298 → #301, and this
   sets padding, not a background, so the two do not touch at all.

   IT WINS THE CASCADE TWICE OVER, which is worth saying because a scoped rule
   that silently loses is the classic failure here. Tailwind emits `.py-3` inside
   `@layer utilities`; a Blazor scoped stylesheet is unlayered, and unlayered
   normal declarations beat layered ones outright. Even if the layer went away,
   `[b-xxxxx] tr > td` is (0,1,2) against `.py-3`'s (0,1,0).

   IT MATCHES THE SKELETON ROW TOO, and that is the reason the selector is not
   `tr.kts-dt-row > td`. DataTable puts `kts-dt-row` on BODY rows only: its
   IsLoading branch renders `<tr class="border-b border-gray-50">` with
   `<td class="px-4 py-3">` and no row class at all. A selector keyed to
   `kts-dt-row` therefore leaves the shimmer rows at Foundation's py-3 while the
   real rows repaint at this padding — the table would jump by the difference,
   ten rows' worth, on every date change (IsLoading is bound on this page's
   table, and the LoadingTemplate above renders a second one at PageSize=10).
   Two elements deep is enough here: everything this rule can reach is inside
   the card that wraps the table, which is what carries the scope attribute.

   THE ROW STAYS A COMFORTABLE POINTER TARGET, and the figure is COMPUTED rather
   than written here: Tests/Admin.Web.Tests/Scheduling/AgendaRowDensityTests
   resolves this declaration, reads the class names the time cell is built from
   out of Pages/Agenda.razor, resolves each of them plus Tailwind's own
   `--spacing` out of wwwroot/css/tailwind.css, and adds up the
   shortest row this table can render — the time cell's stack of `HH:mm` over the
   status badge, which is taller than the actions button and taller than a
   single-line patient name. It fails if that total drops under
   AgendaTableLayout.MinimumRowHitAreaPx, and it fails if this padding is not
   smaller than the `py-3` it replaces. No number from that arithmetic is
   recorded here, because a number written beside a declaration is a number that
   stops being true the moment either side of it moves.

   NOTHING ABOUT THE HIGHLIGHT WASH MOVES. This block changes box metrics only —
   no ink, no background, no token — so every ratio recorded above still
   describes what ships, and AgendaAttentionRowContrastTests re-derives all of
   them from the pinned palette on every run rather than trusting that sentence.
   ──────────────────────────────────────────────────────────────────────── */
[b-o4y2311xnc] tr > td {
    padding-block: 0.25rem;
}

/* ────────────────────────────────────────────────────────────────────────────
   RESOURCE CELLS — the other half of #1261.

   The Medic / Cabinet / Ecograf columns are generated from the clinic's own
   schema list and used to declare `Width = "110px"` each. Nothing that names a
   doctor or a room fits in 110 px, so those cells wrapped onto two and three
   lines while the fixed time and actions columns kept every pixel they claimed.
   Pages/Agenda.razor now declares no width for them at all — they are the
   columns that absorb what the two fixed ones leave — and this band is what
   keeps that from meaning "as narrow as the shortest value" or "as wide as the
   longest one".

   Both lengths and the class name are declared in Helpers/AgendaTableLayout and
   consumed by the markup from there; AgendaTableLayoutWiringTests ties this end
   to that one, so renaming either goes red instead of quietly disagreeing.

   THE COLUMN MUST BE ABLE TO SHRINK, and that is what decides the mechanism.
   This table is auto-layout (`<table class="w-full text-sm text-left">`, no
   `table-layout: fixed`), so no column ever renders narrower than the
   min-content contribution of its cells. `white-space: nowrap` — the obvious
   way to get an ellipsis — makes that contribution the WHOLE string, clamped
   only by max-width: the column's floor stops being a number this file declares
   and becomes however long the clinic's longest doctor name is, up to 18rem
   each. Three or four resource columns then push the table's own minimum past
   the content area at 1280px and the fixed 260px actions column — the
   receptionist's primary control — goes off to the right. That is the OPPOSITE
   of "the resource columns absorb what is left"; it is them taking it.

   So the ellipsis comes from a one-line clamp over text that is still allowed
   to wrap, not from nowrap:

     `overflow-wrap: anywhere` puts a soft wrap opportunity between characters,
     and — unlike `break-word` — CSS Text 3 counts those when computing
     min-content. The cell's min-content contribution collapses to a character,
     so `min-width` below is the floor, a declared constant, whatever the value
     says.

     `-webkit-line-clamp: 1` over `display: -webkit-box` shows the first line
     and ellipsises the rest, so a name too long for the column truncates on ONE
     line — the row stays as short as the density block above makes it. This is
     the same declaration set Tailwind's own `line-clamp-N` emits, and
     `.line-clamp-2` ships in this app's built wwwroot/css/tailwind.css, which is
     where AgendaColumnWidthTests reads the shape rather than trusting this
     paragraph.

   THE CEILING IS STILL LOAD-BEARING, for its own reason rather than that one: a
   resource column with room to grow would otherwise take all of it, and every
   pixel it takes past a readable name is a pixel pushed onto the actions
   column. 18rem holds `Dr. Alexandra Popescu-Ionescu` un-truncated.

   WHAT AN OVER-WIDE TABLE ACTUALLY DOES, since an earlier version of this
   comment got it wrong and a wrong comment is a defect in its own right: it
   scrolls, it does not clip. DataTable wraps its `<table>` in a `<div>` whose
   class is `ScrollWrapperClass`, which at the pinned KTS.Foundation 0.68.0
   returns `overflow-x-auto` when neither MaxHeight nor ExternalScrollContainer
   is passed — which is how Pages/Agenda.razor calls it — and
   `.overflow-x-auto{overflow-x:auto}` is in the built tailwind.css this app
   links. So the page's `border rounded-xl overflow-hidden` card is never the
   thing that meets the overflow, and the issue's edge case for a four-schema
   clinic ("the table scrolls horizontally inside its own container, never the
   page body") is already true. Horizontal scroll is still a cost — it is width
   the receptionist has to drag for — which is why the floor is a constant here
   and not a name length.
   ──────────────────────────────────────────────────────────────────────── */
[b-o4y2311xnc] .agenda-resource-value {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 1;
    overflow: hidden;
    overflow-wrap: anywhere;
    min-width: 7.5rem;
    max-width: 18rem;
}

/* ────────────────────────────────────────────────────────────────────────────
   RESOURCE HEADERS — the other contributor to the same minimum.

   Caught by the second Copilot round on PR #1314, and it is the same defect the
   block above fixes, one row higher up. A column's min-content in an auto-layout
   table is the WIDEST min-content among all of its cells — and the <th> is one
   of them. The rule above collapses the BODY cell's contribution to a character
   and hands the floor to `min-width`; the header was left alone, rendered by
   DataTable as `<th class="px-4 py-3 font-medium">` with no wrapping rule of its
   own. So its min-content stayed its longest WORD, and a clinic whose resource
   type is one long word — `Electrocardiograf`, `Radiodiagnostic` — put the
   column's floor back under the control of the schema's name, which is the exact
   thing #1261's "a clinic that renamed its resource types" row forbids.

   `overflow-wrap: anywhere` is the same mechanism and it is chosen for the same
   reason: CSS Text 3 counts the soft wrap opportunities it introduces when
   computing min-content (`break-word` explicitly does not), so the header's
   contribution collapses to a character and the column's floor is the constant
   AgendaTableLayout.ResourceMinWidth declares, whatever the header says.

   NO ELLIPSIS HERE, deliberately. The body cell clamps to one line because a
   truncated value is still identifiable from its first characters and the full
   string is on `title`. A header is the column's only label — truncating it
   would leave the receptionist unable to tell which resource a column is. It
   wraps instead: under pressure the header row gets taller, which costs the same
   pixels once for the whole table rather than once per row.

   IT APPLIES TO EVERY HEADER IN THIS TABLE, not only the generated ones, and
   that is a choice rather than a limitation. DataTableColumn on KTS.Foundation
   0.68.0 DOES expose a per-column `HeaderTemplate` — the reflected property list
   is Id, Title, PropertySelector, Width, Sortable, Filter, FilterOptions, Format,
   CellTemplate, HeaderTemplate — and it is rendered, in all three of the header's
   branches (sortable, filterable, plain). A template would scope this to the
   generated columns only, at the cost of a second markup/stylesheet contract to
   keep in step and of depending on a component parameter's runtime behaviour that
   this repo cannot execute — the trap this very page carries a note about at its
   `Sort` parameter, which compiles and does nothing in this table's mode (#1301).
   A property that inherits needs neither. The breadth costs nothing: the time and
   actions columns declare a Width, so this only ever lets them shrink toward it
   under pressure, and it never widens anything.

   IT REACHES THE TEXT THROUGH FOUNDATION'S OWN WRAPPERS. DataTable does not put
   the header string straight in the <th>: it nests it in
   `<span class="inline-flex items-center gap-1">`, inside a `<button>` when the
   column sorts. `overflow-wrap` is an INHERITED property, so one declaration on
   the <th> reaches the text in every one of those branches — which is the second
   reason it is the mechanism here and a class on one wrapper is not.
   ──────────────────────────────────────────────────────────────────────── */
[b-o4y2311xnc] th {
    overflow-wrap: anywhere;
}

/* THE OVERRIDE MARKER'S RULES ARE NOT HERE ANY MORE (#1456 review round 8).
   `.agenda-override*` moved to Components/Scheduling/AgendaOverrideNote.razor.css
   when its markup moved out of the patient CellTemplate, because Blazor CSS
   isolation stamps each component's OWN scope attribute: rules kept here would
   have been rewritten to `[b-<page>]` and matched nothing, silently. What stays
   here is the checked-out row wash below — that is the <tr>, and the <tr> is
   still this page's. AgendaOverrideMarkerContrastTests reads the wash from this
   file and the marker's inks from that one, so it still measures both. */

/* ────────────────────────────────────────────────────────────────────────────
   THE TABLE'S SCROLL BOX (#1260).

   NO ::deep, and that is not an oversight — the same distinction the extracted
   override stylesheet draws. This div is authored in Agenda.razor, so the Razor
   compiler stamps the page's scope attribute onto it and a plain class selector
   matches. Only the elements DataTable renders (`tr`, `th`) need reaching
   through.

   `agenda-table-region` is declared in Helpers/AgendaTableLayout, not spelled
   out here, and AgendaFullDayTableTests ties the two ends together — the
   arrangement AgendaTableLayout.ResourceCellClass already established, so
   renaming one end goes red instead of silently unstyling this region.

   THIS IS THE ONLY SCROLL CONTAINER in the table's stack, in both axes, at
   every width. The page passes ExternalScrollContainer="true", under which
   Foundation emits its own wrapper with neither overflow nor max-height, so
   `overflow: auto` here is not nested inside anything — which is what keeps the
   last row reachable and what makes Foundation's own `position: sticky` <thead>
   stick to the top of THIS box. Do not add a MaxHeight to the DataTable beside
   it.

   `overflow` sits OUTSIDE the media query on purpose: it is the declaration
   that carries the HORIZONTAL case, and a four-schema table is wider than a
   phone. Below the breakpoint the region has no height constraint at all, so it
   sizes to its rows and only the sideways axis ever scrolls.

   No `overflow-hidden` any more: it was here to clip the card's rounded corners
   and `overflow: auto` clips them just the same.
   ──────────────────────────────────────────────────────────────────────── */
.agenda-table-region[b-o4y2311xnc] {
    overflow: auto;

    /* ── EDGE TO EDGE (#1509) ──────────────────────────────────────────────
       The fill and the border used to be an inline `style` on the div, next
       to `border rounded-xl`. They are declarations, so they belong in a
       stylesheet where a test can read them — which is the whole reason the
       contrast rows below can be computed rather than asserted.

       TOP HAIRLINE ONLY, and each of the four is a separate decision:

         · `border-top` is KEPT even though Calendar's data surface has none.
           In DARK theme the sticky <thead> Foundation renders carries
           `bg-white`, and Foundation's theme rewrites that to
           `background-color: var(--kts-body-bg) !important` — the PAGE
           background. With the card edge gone there would then be nothing at
           all between the page and the header band. Measured: --kts-body-bg
           against --kts-card-bg is 1.09:1 in dark, far under the 3:1
           non-text floor, so the boundary cannot be carried by the fills.
           This hairline is what carries it. See
           AgendaEdgeToEdgeContrastTests rows C1 and C2.
         · NO `border-bottom`: the status bar directly below already draws
           `border-t border-[var(--kts-border-color)]`, and a second line
           renders a double hairline Calendar does not have.
         · NO side borders and NO radius: that IS the card chrome being
           removed. A radius on a full-bleed surface clips its own corners
           against the viewport edge.

       `border-inline: 0` and `border-bottom: 0` are written out rather than
       left to the initial value. `border-top` alone sets `border-top-style`
       only; the other three keep style `none` and would render nothing
       anyway — but the shorthand's absence is not a statement, and this file
       is read by a conformance test that checks what is DECLARED. Stating
       them is what makes "no left or right border" assertable.
       ────────────────────────────────────────────────────────────────── */
    background-color: var(--kts-card-bg);
    border-top: .0625rem solid var(--kts-card-border);
    border-bottom: 0;
    border-inline: 0;
    border-radius: 0;
}

/* ────────────────────────────────────────────────────────────────────────────
   THE STICKY HEADER'S OWN BOUNDARY (#1509, contrast row C2).

   ::deep is REQUIRED: DataTable renders the <thead>, so it never carries this
   page's scope attribute — the wrapper <div> around the table does.

   WHY IT IS HERE, and what it does and does NOT achieve. Foundation renders
   `<thead class="sticky top-0 bg-white z-10">`, and its theme rewrites that to
   `background-color: var(--kts-body-bg) !important` in dark. Measured against
   the row fill (`--kts-card-bg`):

       light  #fff      vs #fff      = 1.00:1   (the SAME colour)
       dark   #1a1b1e   vs #1f2024   = 1.06:1

   i.e. the header band has never been separable from the rows by fill, in
   either theme, before this change or after it — the two tokens are equal in
   light for every card in the app. #1509 does not alter either value; it alters
   what surrounds the region, so this pair is re-measured rather than assumed.

   This rule is the alternative treatment #1509 names ("a border-bottom on the
   thead is required"). It is an improvement — from no boundary at all to the
   same hairline every other boundary in this app draws — and it is NOT a pass:
   `--kts-card-border` measures 1.20:1 light / 1.18:1 dark against the fills
   either side, under 1.4.11's 3:1. Reaching 3:1 would need ink around
   `--kts-text-muted` strength for a table rule, which is a visual-design
   decision this issue does not authorise, and any literal would violate its own
   "no colour literal" rule. Recorded as unmet in the PR rather than papered
   over, and derived — not retyped — by AgendaEdgeToEdgeContrastTests.
   ──────────────────────────────────────────────────────────────────────── */
[b-o4y2311xnc] thead th {
    border-bottom: .0625rem solid var(--kts-card-border);
}

/* ────────────────────────────────────────────────────────────────────────────
   THE WORKSPACE COLUMN (#1509).

   NO PADDING, and that absence is the declaration. It carried `p-6` and
   `gap-6` as utilities until #1509; every band owns its own inset now, which
   is what lets the table reach the viewport edge and the status bar sit flush
   with the page bottom (Δ = 0 against Calendar, where it used to be 3rem —
   1.5rem of `gap-6` above the strip and 1.5rem of `p-6` below it).

   `min-width: 0` is the one thing it does declare, and it is load-bearing
   rather than defensive: this div is a flex child of Foundation's <main>
   column and it contains a horizontally-scrolling region that is routinely
   wider than the viewport (the floor is ~880px at five columns and ~1250px at
   eight). Without it the column can be sized by that content instead of by
   the viewport, and the scroll moves to an ancestor that is
   `overflow-x: hidden` — i.e. the last columns become unreachable rather than
   scrollable. `.agenda-table-region` must stay the only scroll owner in both
   axes (#1260).

   The class name comes from Helpers/AgendaTableLayout.ShellClass, not spelled
   out here — same two-ended arrangement as the region above.
   ──────────────────────────────────────────────────────────────────────── */
.agenda-shell[b-o4y2311xnc] {
    min-width: 0;
}

/* ────────────────────────────────────────────────────────────────────────────
   THE STAT BAR (#1509) — the page's one chrome band above the data surface.

   It keeps an inline inset where the table gives one up, because it is chrome
   and not data. Calendar's own precedent is `.global-doctor-filter-bar`
   (`padding: .5rem 1rem`), a 1rem-inset filter row directly above a zero-inset
   grid. Four stat cards flush to the viewport edge read as broken.

   Asymmetric block padding (1rem over, .75rem under) rather than a `gap` on
   the shell: the cards' own border sits closer to the table's top hairline
   than to the page header, so an even gap reads bottom-heavy. The `gap-4`
   BETWEEN the cards is still a utility on the grid itself.

   `flex: none` so the bar keeps its natural height when the region beside it
   takes `flex: 1` from the `sm` breakpoint up. Without it the bar is a flex
   item with `flex-shrink: 1` and gets compressed on a short viewport — the
   numbers, not the table, would be what shrinks.
   ──────────────────────────────────────────────────────────────────────── */
.agenda-stat-bar[b-o4y2311xnc] {
    flex: none;
    padding: 1rem 1rem .75rem;
}

/* ────────────────────────────────────────────────────────────────────────────
   ...AND THE SINGLE-SCREEN SIZING, FROM THE `sm` BREAKPOINT UP (#1260).

   40rem is what Tailwind v4 compiles `sm:` to — its own generated stylesheet
   emits `@media (min-width:40rem)` — so this query and the `sm:flex-1
   sm:min-h-0` the shell div wears switch on at exactly the same width, as does
   the filter grid's `sm:grid-cols-2`. Three halves of one layout, one
   breakpoint; AgendaFullDayTableTests asserts both ends of it. rem rather than
   px is also what this stylesheet is required to express lengths in
   (CalendarStylesheetConformanceTests, #1018), and it is the better unit here
   for the same reason it is there: a media query in rem answers to the reader's
   own font size.

   WHY GATE IT AT ALL. Stacked, the four filter cards plus `p-6` and a `gap-6`
   are most of a phone viewport of chrome before the table starts, and
   Foundation's MainLayout puts KtsMobileBottomNav under it. The shell would
   overflow anyway, so <main> would scroll the whole page — filter cards off the
   top, which is the complaint #1260 exists to fix — while this region sat
   pinned at its floor: a 12rem scroll box inside a scrolling page. Below the
   breakpoint the page is therefore left as it was before #1260, one document
   scroll, no nesting.

   `flex: 1 1 0` rather than a height: the region is a flex child of a column
   that is itself a flex child of Foundation's <main>, so it takes exactly the
   height the header and the filter cards leave. Nothing here reads the
   viewport, so nothing here goes stale when the page header wraps to two lines
   or the failure banner appears above it.

   `min-height` is what stops "leftover" from meaning "nothing" on a short
   window or at 200% zoom: below it the region refuses to shrink further and
   <main> scrolls as a whole instead, which is the outcome #1260's
   short-viewport edge case names as acceptable. 12rem is the sticky header plus
   roughly five rows at #1261's density — a design floor rather than a computed
   one, and deliberately not written beside a number from AgendaRowDensityTests'
   arithmetic, for the reason the rest of this file gives: a number written next
   to a declaration stops being true the moment either side of it moves.
   ──────────────────────────────────────────────────────────────────────── */
@media (min-width: 40rem) {
    .agenda-table-region[b-o4y2311xnc] {
        flex: 1 1 0;
        min-height: 12rem;
    }
}

/* ────────────────────────────────────────────────────────────────────────────
   PRINTING (#1509).

   Currently broken, and #1509 is the right place to fix it because it is the
   change that touches these three declarations. An `overflow: auto; flex: 1;
   min-height: 12rem` box prints ONE VIEWPORT of rows — everything below the
   fold of the scroll box is clipped by the printer exactly as it is clipped on
   screen, with no scrollbar to reveal it. A receptionist printing the day gets
   the first five appointments and no indication that there were forty.

   All three are unset rather than adjusted:
     · `overflow: visible` is what lets the print engine paginate the rows
       across sheets instead of clipping them.
     · `flex: none` and `min-height: 0` stop the region claiming (or reserving)
       a share of a viewport height that has no meaning on paper.
     · `border: 0` removes the top hairline, which is a boundary against the
       page background — a fact about the screen only.

   The status bar's own print rules are NOT here: it is a child component with
   no root element, so `::deep` has no ancestor to hang this page's scope
   attribute on and Foundation's FAB is unreachable from this file. They live
   in Styles/_status-filter-bar.scss, global, with the reasoning.
   ──────────────────────────────────────────────────────────────────────── */
@media print {
    .agenda-table-region[b-o4y2311xnc] {
        overflow: visible;
        flex: none;
        min-height: 0;
        border: 0;
    }
}

/* ────────────────────────────────────────────────────────────────────────────
   A ROW THAT WILL NOT OPEN DOES NOT LOOK AS THOUGH IT WOULD — #1262.

   Binding `OnRowActivated` makes DataTable render EVERY body row as
   `<tr class="… kts-dt-row-activatable" tabindex="0" onclick=… onkeydown=…>`.
   That decision is per TABLE and not per row, so a row whose appointment came
   back without a patient object — Helpers/AgendaAppointmentProjection writes
   `Patient?.Id.ToString() ?? Guid.Empty.ToString()` — gets the pointer cursor
   and the hover fill of a row that opens a chart, while
   Helpers/AgendaRowActivation refuses to navigate it. This block is what stops
   the affordance lying about that; the class it keys on is
   AgendaRowActivation.InertRowClass and reaches the <tr> through
   DataTable's RowClass hook, composed with #1265's highlight.

   ::deep is REQUIRED, for the same reason the two blocks above document: the
   <tr> is rendered by DataTable, so it never carries this page's scope
   attribute — the wrapper <div> around the table does.

   BOTH RULES WIN THE CASCADE, and neither wins it on specificity. Foundation
   ships `.kts-dt-row-activatable{cursor:pointer}` and
   `:where(.kts-dt-row:hover){background-color:var(--kts-surface-hover)}` inside
   `@layer components`; a Blazor scoped stylesheet is unlayered, and unlayered
   normal declarations beat layered ones outright whatever the selectors weigh.
   (The hover rule is `:where()`-wrapped and weighs nothing anyway — the same
   zero-specificity fill the highlight above relies on out-specifying.)

   `:not(.row-pulse-attention)` on the hover rule, because a checked-out
   appointment can also be one the API returned without a patient, and the two
   classes then land on the same <tr>. Without the exclusion this rule would
   erase #1265's 18% warning wash on hover — a highlight that vanishes under the
   pointer is worse than one that does not react to it. Source order does not
   decide this: both rules are unlayered and `:hover` would out-weigh the plain
   class either way, which is why the exclusion is written rather than relied on.

   THE FOCUS RING IS DELIBERATELY LEFT ALONE. Foundation's
   `tr.kts-dt-row-activatable:focus-visible{outline:2px solid var(--kts-primary)}`
   is unlayered in the package and applies to inert rows too. It should: the row
   is still a tab stop (tabindex is Foundation's per-table decision), and a
   focusable element with no visible focus indicator fails WCAG 2.4.7 whether or
   not activating it does anything. What the row answers a keypress with is the
   toast in Pages/Agenda.razor.cs, not silence.

   NO INK MOVES HERE — no colour, no token, no wash depth — so every ratio
   recorded against the highlight above still describes what ships, and
   AgendaAttentionRowContrastTests re-derives all of them on every run rather
   than trusting this sentence.
   ──────────────────────────────────────────────────────────────────────── */
[b-o4y2311xnc] tr.agenda-row-inert {
    cursor: default;
}

[b-o4y2311xnc] tr.agenda-row-inert:not(.row-pulse-attention):hover {
    background-color: transparent;
}
/* /Pages/Calendar.razor.rz.scp.css */
/* ────────────────────────────────────────────────────────────────────────────
   Calendar page styles — Foundation-conformant (#1018).

   Every length here is `rem`, so the whole surface scales with the user's root
   font size (WCAG 1.4.4). Every ink is a Foundation semantic token, so it flips
   with the theme. `#hex` appears only as a `var(--token, #fallback)` fallback,
   or inside a comment recording a measurement — never as a colour of its own.

   Reference implementation for that standard: `Pages/Agenda.razor.css`.
   Foundation figures quoted below are read from KTS.Foundation 0.62.0 — the
   scoped-CSS bundle at `staticwebassets/*.bundle.scp.css` (the infix is a
   per-build fingerprint, so match the pattern rather than a literal name) and
   the palette at `staticwebassets/css/kts-foundation-theme.css`.
   ──────────────────────────────────────────────────────────────────────── */

.event-calendar[b-igas6ldote] {
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: hidden;
}

.component-wrapper[b-igas6ldote] {
    flex: 1;
    min-height: 0;
    overflow: hidden;
}

.scheduller-patients[b-igas6ldote] {
    flex: 1;
    min-height: 0;
    height: 100%;
    overflow: hidden;
}

.sidebar-content[b-igas6ldote] {
    height: 100%;
    overflow: hidden;
}

.schedule-container[b-igas6ldote] {
    height: 100%;
    overflow: hidden;
}

[b-igas6ldote] .cal-scheduler {
    height: 100%;
    /* KtsScheduler's own CSS caps its scrollable grid at a fixed 42rem via
       --kts-scheduler-max-height (see KtsScheduler.razor.css .kts-scheduler-scroll)
       regardless of how much room the flex chain above actually gives it —
       that's why the grid stopped short of the legend instead of filling the
       page. Override the token to let it use all available height. */
    --kts-scheduler-max-height: 100%;
}

/* Full-bleed: the scheduler fills the page body, so Foundation's card chrome
   (1px border + --kts-border-radius-lg) would draw rounded corners inside a
   square, edge-to-edge parent. Foundation exposes no --kts-scheduler-* var for
   radius/border (only max-height, gutter-width, daycol-min, event-accent), so
   flatten it here. The extra .kts-scheduler class raises specificity above
   Foundation's own scoped .kts-scheduler rule regardless of bundle order. */
[b-igas6ldote] .cal-scheduler.kts-scheduler {
    border: 0;
    border-radius: 0;
}

[b-igas6ldote] .cal-scheduler .kts-scheduler-scroll {
    flex: 1;
    min-height: 0;
}

/* ── Status bar width (#1084) ──
   Foundation hardcodes `border-left-width: 3px` on .kts-scheduler-event and
   publishes no --kts-scheduler-* var for it (the scheduler's tokens are
   max-height, gutter-width, daycol-min, event-accent, event-bar and the six
   preview inks — none of them a width), so widening the bar has to be an
   override here rather than a token this page sets.

   3px is a hairline: on a day-view card it reads as an edge treatment on the
   card, not as a region carrying its own meaning, which is half of why status
   went unnoticed even after #1013 gave it a channel. .3125rem is 5px at the
   16px default root — enough to read as a band, and deliberately NOT the 8px
   of the option mock-up, which starts to eat width a 15′ card needs for the
   patient name.

   rem rather than px so the bar scales with the user's root font size, which is
   the standard the rest of this file holds to (WCAG 1.4.4) — and Foundation's
   own px value is what makes the difference visible: a user at a 20px root gets
   a 6.25px bar here where Foundation would have held them at 3.

   SPECIFICITY, because this has to win without !important: Foundation's rule is
   `.kts-scheduler-event[b-2o6ubl5q3y]`, one class plus the scope attribute, so
   (0,2,0). `::deep` puts this page's scope attribute on the ancestor, emitting
   `[b-…] .cal-scheduler .kts-scheduler-event` — (0,3,0) — which wins regardless
   of bundle order. Verified against the emitted
   `obj/…/scopedcss/Pages/Calendar.razor.rz.scp.css` rather than assumed —
   ONCE, BY HAND. Nothing re-checks the emitted file on every run.
   CalendarStatusSignalWiringTests reads this AUTHORED stylesheet and the
   Foundation bundle, which catches the width and the `::deep` spelling but
   not the scope attribute Razor actually emits.

   THE FOUNDATION-UPGRADE TRAP: because this wins on specificity, a future
   Foundation release that changes its own bar width — or introduces the
   --kts-scheduler-event-bar-width token this rule exists for want of — will
   have no visible effect here, silently. If you are reading this during a
   Foundation bump: check whether that token now exists, and if it does, delete
   this rule and set the token on .cal-scheduler alongside
   --kts-scheduler-max-height above.

   Previews are not excluded, and do not need to be: `.is-preview` is only drawn
   when IsPreviewSelector is bound, and this page does not bind it (the drag
   preview lives on the booking flow's OverrideAppointment, whose own scoped CSS
   this cannot reach). */
[b-igas6ldote] .cal-scheduler .kts-scheduler-event {
    border-left-width: .3125rem;
}

/* ────────────────────────────────────────────────────────────────────────────
   Event template (rendered inside KtsScheduler's .kts-scheduler-eventpos)

   ── Content budget (#1009, re-derived by #1110 / #1111 / #1114) ──
   The tier is a function of the card's BUDGET IN PIXELS, not of its duration.
   The arithmetic is C# — Services/Scheduling/EventCardLayout.cs — because it
   has to read the clinic's density token and because it is the only form this
   repo can unit-test (no bUnit). What follows is the derivation it encodes;
   the px figures below are quoted, and comments are stripped before
   CalendarStylesheetConformanceTests looks for raw px, which is why they may
   appear here and in no declaration.

   Foundation's chrome, verified against the shipped 0.65.0 bundle and
   re-verified unchanged against 0.68.0 (#1243):

       .kts-scheduler-eventpos          padding 1px 2px 1px 1px  -> 2px vertical
       .kts-scheduler-lanerow eventpos  padding 1px 1px 2px 1px  -> 3px vertical
                                                                   2px horizontal
       .kts-scheduler-event             padding .1875rem .375rem -> 6px vertical
                                                                  12px horizontal
                                        min-height 1.125rem (18px), border-box
                                        gap .125rem (2px), column
       ::deep .cal-scheduler event      border-left-width .3125rem (5px)

   -- Day / WorkWeek / GroupedDay: duration drives HEIGHT --

       content height = max( H * minutes/60 - 8 ,  12 )

   H is --kts-scheduler-interval-height in px: 56 Compact / 80 Standard /
   112 Comfortable (#1014, #1082). The 12px floor is the 18px `min-height`
   MINUS the event's own 6px vertical padding, because min-height sits on the
   padded box.

   This comment previously stated the formula as `max(56 * m/60, 18) - 8`,
   which yields 10px at 15' while its own table said 12px. The table was right
   and the formula was not — it paid the eventpos padding twice. That table
   also read `40' -> 27px`; 56 * 40/60 - 8 = 29.3px. Both are corrected here.

   -- TimelineDay: the two axes swap (#1114) --
   Every lane row is minmax(3.5rem, 1fr), so height is CONSTANT — 56 - 3 - 6 =
   47px for a 15' appointment exactly as for a 90' one — and height encodes
   nothing about duration. Width is the scarce axis and the one duration drives:

       content width = W * minutes/60 - 19        (19 = 5 bar + 12 + 2)

   W is --kts-scheduler-interval-width in px: 96 Compact / 160 Standard /
   224 Comfortable — the last two re-derived by #1114 from the MEASURED advance
   width of a Romanian patient name at .75rem/600 in Inter, plus the ellipsis
   that marks the truncated remainder. In the day-column views the card spans
   its lane and width is not budgeted here at all (the GroupedDay column budget
   is #1102's).

   -- What one row costs --
   At the Foundation event type scale (.kts-scheduler-event-title = .75rem/1.2,
   .kts-scheduler-event-time = .6875rem):

       .patient-header  micro   .625rem, line-height 1  -> 11.5px
       .patient-header  full    .75rem x 1.2            -> 14.4px
       .service                 .6875rem x 1.2          -> 13.2px
       .room                    .6875rem x 1.2          -> 13.2px
       .content-wrap            row gap .125rem         ->    2px

       micro header only                                   11.5px
       full header only                                    14.4px
       full header + service                               29.6px
       full header + service + rooms                       44.8px

   "needs" is painted glyph height, not the line box: at `line-height: 1` the
   glyph content area (~1.15em) is taller than the box it sits in, so the
   is-micro row is budgeted at 10px x 1.15 rather than at its 10px line box.

   Every tier above the floor also demands 2px of slack. That is not padding for
   its own sake: it keeps a tier from being selected on a sub-pixel margin, and
   it is what makes the Compact parity invariant hold EXACTLY — at every
   5-minute step from 5' to 480' the derived tier at Compact equals the tier the
   old hardcoded < 30 / 30-44 / 45-59 / >= 60 rules produced. That sweep is
   asserted in EventCardLayoutTests and is the regression guard for this change.
   The micro tier is the floor and is required only to FIT (11.5px into 12px),
   because there is no smaller tier to fall to.

   -- The derived tiers, all three densities --

       duration   Compact          Standard         Comfortable
        15'       12  micro        12  micro        20  header
        20'       12  micro        18.7 header      29.3 header
        25'       15.3 micro       25.3 header      38.7 header+svc
        30'       20  header       32  header+svc   48  all three
        40'       29.3 header      45.3 header+svc  66.7 all three
        45'       34  header+svc   52  all three    76  all three
        60'       48  all three    72  all three    104 all three

   The 15'-at-Comfortable row is #1110's reported symptom: 20px of budget,
   more than the 14.4px a full-size header needs, and it rendered at 10px.

   -- Fields on the header line --
   Rows come from the height budget; the fields on the header line come from
   the WIDTH budget, name first, then duration, then time. In the day-column
   views width is unbudgeted so every field is emitted; in TimelineDay the
   ladder does the work, which is why a 30' timeline card no longer spends
   ~64px of a 77px budget on a time and a duration chip before the name gets
   anything. The micro tier drops the start time and keeps the duration
   (#1111 section 2, option (b)) — the rationale is in Calendar.razor.

   Nothing is ever half-rendered: a row that would not fit is not emitted at
   all, and every emitted row is `nowrap` + ellipsis so it cannot wrap into a
   second line box and blow the budget. Everything suppressed on the card is
   still on the QuickInfo tooltip, which lists time, service, every resource
   and the status.

   If a future Foundation release changes any padding above, the change surfaces
   in EventCardLayoutTests as a failed budget assertion rather than as a clipped
   card — that is why the box model is named there constant by constant.

   ── Ink (#1008) ──
   Foundation paints the card as
       color-mix(in srgb, var(--kts-scheduler-event-accent) 14%, var(--kts-card-bg))
   (24% on hover), where --kts-card-bg is #ffffff light and #1f2024 dark. The
   four text rules used to be a fixed light-theme grey ramp, which measured
   1.07:1 / 1.61:1 / 1.07:1 / 1.46:1 in dark theme — functionally invisible.

   They are tokens now, chosen against the WORST-CASE fill rather than against
   any particular accent. That bound is what makes the choice survive #1007:
   the accent is a KtsIdentityPalette entry now rather than the old #e5e7eb
   fallback, and a 14–24% blend can never produce a fill outside the bound set
   by mixing pure black (light) or pure white (dark) in — so every one of the
   thirty entries, and the "no medic assigned" neutral, are already inside it.
   Measured at that bound (WCAG 2.1 relative luminance, color-mix evaluated
   arithmetically; see CalendarEventContrastTests):

       light, accent #000000, hover fill #c2c2c2   heading  9.16:1   body 5.77:1
       dark,  accent #ffffff, hover fill #555659   heading  6.74:1   body 4.98:1

   --kts-text-body is used for the secondary ink rather than --kts-body-color:
   at that same worst-case light hover fill --kts-body-color (#475569) measures
   only 4.25:1 and fails AA, while --kts-text-body (#374151 light / #cbd5e1
   dark) clears it in all four cases. Agenda.razor.css made the same swap for
   the same reason.
   ──────────────────────────────────────────────────────────────────────── */

/* .template-wrap USED TO BE the hit target app.js hung off (`closest(".template-wrap")`), and
   this comment said to keep the class and the `id` on it for that reason. #1380 deleted that
   consumer: the `closest()` call lived in `attachHoverEvents`, ~170 lines driving the old
   vendor Scheduler DOM which nothing had called since the KtsScheduler migration.

   So the constraint is gone. The class stays because the rules below style it. The `id` on
   `Pages/Calendar.razor:311` now has NO reader anywhere — its only two were that `closest()`
   and the `getElementById` in `adjustQuickInfoPosition`, both deleted — and it is left in place
   deliberately rather than swept: removing an `id` from rendered markup is a DOM change, not a
   remnant removal, and it wants its own decision. What must not survive is this comment telling
   the next reader that a dead integration requires it. */
.template-wrap[b-igas6ldote] {
    width: 100%;
    height: 100%;
    min-width: 0;
}

.content-wrap[b-igas6ldote] {
    display: flex;
    flex-direction: column;
    gap: .125rem;
    min-width: 0;
    max-width: 100%;
    overflow: hidden;
}

/* Left-aligned, single line. The status dot that used to be pushed to the far
   edge by `justify-content: space-between` is gone (#1013) — on a day-view
   card spanning most of the viewport it stranded several hundred px of dead
   space in the middle of the row, which is half of why the card read as empty
   (#1009). Status now lives on the tooltip; see the note in Calendar.razor. */
.patient-header[b-igas6ldote] {
    display: flex;
    flex-wrap: nowrap;
    align-items: baseline;
    gap: .25rem;
    min-width: 0;
    line-height: 1.2;
}

.time[b-igas6ldote] {
    font-size: .6875rem;
    color: var(--kts-text-body);
    font-weight: 500;
    flex: 0 0 auto;
    font-variant-numeric: tabular-nums;
}

.patient[b-igas6ldote] {
    font-size: .75rem;
    color: var(--kts-heading-color);
    font-weight: 600;
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.duration[b-igas6ldote] {
    font-size: .6875rem;
    color: var(--kts-text-body);
    font-weight: 400;
    flex: 0 0 auto;
    font-variant-numeric: tabular-nums;
}

/* The micro tier: a card with under ~16.4px of usable height. The 14.4px
   default header line box does not fit, and neither does a .6875rem one at
   line-height 1 once the glyph content area (~12.7px) is counted. .625rem/1
   paints ~11.5px and does.

   .time is NOT listed below because the template does not emit it in this tier
   (#1111 section 2, option (b)): the row already sits at the appointment's hour
   and the grid has a time gutter, so the start time is the redundant field and
   the duration is the one nothing else on screen carries. .duration IS listed —
   it now appears here, and at .6875rem its glyphs (~12.7px) would overflow the
   11.5px row. */
.content-wrap.is-micro .patient-header[b-igas6ldote] {
    line-height: 1;
}

.content-wrap.is-micro .patient[b-igas6ldote],
.content-wrap.is-micro .duration[b-igas6ldote] {
    font-size: .625rem;
}

/* ── Cancelled (#1084) ──
   The one signal on this card that is not a colour. Every other status is
   carried by the leading-edge bar's hue alone — measurably so: all 21 pairs of
   status colours sit below 3:1 against each other in both themes, so a user
   discriminating by lightness cannot tell them apart, and the compliant band is
   too narrow to fit seven separable steps (the arithmetic is above the tokens in
   Styles/app.scss). Cancelled is the one state where acting on the wrong card is
   expensive, so it gets a second channel.

   Scoped to the two identity-bearing spans rather than to .content-wrap: a rule
   on the wrapper would strike the service and the room lines as well and turn a
   60′ card into a block of ruled text. .time is deliberately NOT struck — the
   slot is still occupied and the hour is still true.

   .duration is emitted in the micro tier too since #1111, so on a 15′ card this
   selector now strikes both the name and the duration chip — at .625rem/1, two
   short lines rather than one. .time is not emitted in that tier at all, so it
   is not a third.

   text-decoration-thickness is pinned rather than left at `auto`, which resolves
   from the font and lands nearer 2px at these sizes — heavy enough over a
   .625rem name to close the counters in a/e/o. .0625rem is 1px at the default
   root and scales with it, holding the rule the rest of this file keeps.

   Truncation needs no help: the decoration is painted over the element's own
   line box, and .patient is `overflow: hidden` + `text-overflow: ellipsis`, so
   the line stops where the visible text does and the ellipsis is not struck.

   Announcement is NOT handled here and cannot be. `text-decoration` has no
   semantics — a screen reader says nothing about it — so on its own this would
   be a sighted-only cue, which is the WCAG 1.4.1 failure it exists to avoid.
   Pages/Calendar.razor's GetEventTitle puts the word "cancelled" into the event
   button's accessible name; that is the other half of this feature, and neither
   half is sufficient alone. */
.content-wrap.is-cancelled .patient[b-igas6ldote],
.content-wrap.is-cancelled .duration[b-igas6ldote] {
    text-decoration: line-through;
    text-decoration-thickness: .0625rem;
}

.service[b-igas6ldote],
.room[b-igas6ldote] {
    font-size: .6875rem;
    color: var(--kts-text-body);
    font-weight: 400;
    line-height: 1.2;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.service[b-igas6ldote] {
    font-weight: 500;
}

/* ── The tooltip (#1017: one fragment; #1233: one host) ── */
.quick-info-content[b-igas6ldote] {
    display: flex;
    flex-direction: column;
    gap: .5rem;
}

/* The titled header #1233 folds in from the QuickInfo, which had a header slot the tooltip does
   not — Foundation's .kts-scheduler-tooltip is a single padded box, and `pointer-events: none`,
   so there is nothing to bind and nothing to click. Every value below is Foundation's own
   .kts-scheduler-quickinfo-headmain / -title / -time, verbatim from the shipped 0.65.0 stylesheet,
   rather than a third treatment invented here: the point of the fold is that the tooltip reads as
   the card it replaces. The SURFACE needs nothing at all — .kts-scheduler-tooltip already paints
   --kts-card-bg over --kts-card-border with --kts-box-shadow, which is the card treatment #1233's
   third acceptance criterion asks for. */
.quick-info-head[b-igas6ldote] {
    display: flex;
    flex-direction: column;
    gap: .125rem;
    min-width: 0;
}

.quick-info-title[b-igas6ldote] {
    font-size: .875rem;
    font-weight: 600;
    color: var(--kts-heading-color);
    font-family: var(--kts-heading-font-family);
    overflow-wrap: anywhere;
}

/* tabular-nums so the two clocks line up under the title rather than jittering per glyph. */
.quick-info-time[b-igas6ldote] {
    font-size: .75rem;
    color: var(--kts-body-color);
    font-variant-numeric: tabular-nums;
}

/* No `color` here on purpose: Foundation's .kts-scheduler-tooltip already sets
   `color: var(--kts-body-color)` over `background-color: var(--kts-card-bg)`,
   both theme-aware, so the tooltip never carried the light-theme ramp that
   #1008 found on the card. Restating the token would only add a second place
   to keep in sync. */
.info-row[b-igas6ldote] {
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-size: .8125rem;
}

.info-value[b-igas6ldote] {
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: .5rem;
}

.status-icon-circle-small[b-igas6ldote] {
    width: .75rem;
    height: .75rem;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

/* ────────────────────────────────────────────────────────────────────────────
   Date header template (rendered inside KtsScheduler's .kts-scheduler-dayhead)

   The custom template originally existed to host the medic medallions; #1119
   moved those to the one page-level bar below, so what is left is the day name
   and the day number. The template stays because the number's `is-today` chip is
   this page's, not Foundation's. It has no reason to diverge from Foundation's
   native day head on type scale, so the values below are Foundation's own
   (#1012, folded into #1018):

       .kts-scheduler-dayhead-name  .75rem / 600 / uppercase / .02em
       .kts-scheduler-dayhead-num   .9375rem / 600
       .is-today .. -num            min-width 1.5rem, height 1.5rem,
                                    padding 0 .25rem, border-radius 999px

   Previously .date-day-number was 18px of type in a 32px chip — 20% larger
   type in a 33% larger circle than the rest of the app — and the chip was
   drawn (transparently) on every day, not just today.
   ──────────────────────────────────────────────────────────────────────── */
.doctor-date-header[b-igas6ldote] {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: .125rem;
    padding: .25rem .5rem;
    width: 100%;
}

.date-header-bottom-row[b-igas6ldote] {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-wrap: nowrap;
}

.date-day-name[b-igas6ldote] {
    font-size: .75rem;
    color: var(--kts-body-color, #666);
    text-transform: uppercase;
    letter-spacing: .02em;
    font-weight: 600;
    white-space: nowrap;
}

.date-day-number[b-igas6ldote] {
    font-size: .9375rem;
    font-weight: 600;
    color: var(--kts-heading-color, #212529);
    font-variant-numeric: tabular-nums;
}

/* The chip is drawn only for today, exactly as Foundation does it.

   The ink is --kts-on-brand, Foundation's ink for a saturated brand fill
   (0.65.0, kts.foundation#379; an alias of the older --kts-counter-fg). The
   `#fff` it replaces was chosen to match what Foundation hardcodes on its own
   --kts-primary fills, and that convention is the defect: --kts-primary
   lightens for dark theme (#0f6cbd → #2f81f7) precisely so it reads on a dark
   background, which is the same move that puts white below the floor there.
   Measured on --kts-primary: 5.38:1 LIGHT and 4.60:1 DARK, against 3.75:1
   DARK for `#fff`. The number is 18px/600 — under the 18.66px bold large-text
   threshold, so the 4.5:1 body floor applies, not 3:1.

   The radius is a pill sentinel, not a dimension — it is clamped to half the
   shorter side, so the unit is irrelevant and `rem` is used only to keep the
   file free of `px`. Foundation writes the same sentinel as `999px`. (#1066) */
.date-day-number.is-today[b-igas6ldote] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.5rem;
    height: 1.5rem;
    padding: 0 .25rem;
    border-radius: 999rem;
    background-color: var(--kts-primary, #0d6efd);
    color: var(--kts-on-brand);
}

/* ── The medic medallion bar moved out (#1509) ────────────────────────────────
   .doctor-avatars-row, .doctor-avatar-btn, .doctor-avatar-placeholder,
   .doctor-avatar-initials, .global-doctor-filter-bar and .medic-colour-hint now
   live in Components/MedicFilterBar.razor.css, beside the markup they style.

   They had to move rather than be reached into: the bar renders on Agenda too
   now, and a Blazor scoped stylesheet stamps THIS page's scope attribute onto
   its selectors — so a rule left here would compile to `.doctor-avatar-btn[b-
   <calendar>]` and match nothing at all on the agenda, silently. The rules
   themselves are unchanged; every ratio measured against them still describes
   what ships.
   ──────────────────────────────────────────────────────────────────────────── */

/* ── Per-medic lane headers (#1011) ── */
/* Rendered inside Foundation's .kts-scheduler-lanehead / .kts-scheduler-reshead via
   ResourceHeaderTemplate. Foundation owns the header cell's box; these rules only lay
   out the content this page puts in it, which is why there is no width here — that is
   --kts-scheduler-lanehead-width's job, and it is left at Foundation's default. */
.medic-lane-head[b-igas6ldote] {
    display: flex;
    align-items: center;
    gap: .375rem;
    min-width: 0;
    padding: 0 .5rem;
}

/* The medic's identity colour as a chip. Deliberately not the header's background:
   at 9rem wide that would put the lane name ON the medic's colour, and ink in
   KtsIdentityPalette is per entry (twenty of thirty take dark ink), so no single
   text colour is readable across the palette. The chip carries the identity and the
   name keeps the body ink it already had — which is also why nothing here sets
   `color`. */
.medic-lane-swatch[b-igas6ldote] {
    width: .625rem;
    height: .625rem;
    border-radius: 50%;
    flex-shrink: 0;
}

.medic-lane-name[b-igas6ldote] {
    font-size: .75rem;
    font-weight: 600;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* /Pages/Documents.razor.rz.scp.css */
/* ────────────────────────────────────────────────────────────────────────────
   Pages/Documents.razor.css
   Dropzone functionality migrated to KtsDocumentDropZone from KTS.Foundation.
   ──────────────────────────────────────────────────────────────────────── */
/* /Pages/ResourceDetails.razor.rz.scp.css */
/* ────────────────────────────────────────────────────────────────────────────
   Resource details page — the two rules that actually match something.

   #1060 deleted the other 23 selectors this file carried. They were a stale
   copy of markup that had moved into `Components/MedicHeaderDetails.razor`,
   and Blazor scoped CSS cannot reach a child component with a plain selector:
   the compiler emits `.back-link[b-<thisPage>]`, while the element
   MedicHeaderDetails renders carries `b-<thatComponent>` instead, so the two
   never meet. The live copy, with the worked contrast rationale, is
   `Components/MedicHeaderDetails.razor.css` — fix bugs there.
   ──────────────────────────────────────────────────────────────────────── */

.resource-details-container[b-yj6erecxf7] {
    padding: 2rem;
    overflow-y: auto;
    overflow-x: hidden;
    height: 100%;
}

/* `::deep` is what lets this reach `CustomResourceValueFieldComponent`, which
   renders `<h4 class="section-title mb-3">` under `.resource-details-container`.
   A plain `.section-title` here would compile to `.section-title[b-<thisPage>]`
   and silently match nothing, because the child component's elements carry its
   own scope attribute — the same mechanic documented in Agenda.razor.css.

   Ratios are WCAG 2.1 relative luminance against `--kts-card-bg` (#ffffff
   light / #1f2024 dark), computed by `tools/css-audit/css_theme_audit.py`. */
[b-yj6erecxf7] .section-title {
    font-weight: 600;
    color: var(--kts-heading-color);  /* 16.34 / 14.86 (was #1a1a1a: 17.40 / 1.07) */
    display: flex;
    align-items: center;
    gap: .625rem;
}
/* /Pages/Resources.razor.rz.scp.css */
/*
 * #1276 — the read-only resource detail card.
 *
 * Two rules declared here rather than reached for as Tailwind utilities. `wwwroot/css/tailwind.css`
 * is a COMMITTED, minified artifact, and neither `break-words` nor `text-[var(--kts-warning-text)]`
 * is in it today.
 *
 * It is NOT true that nothing regenerates that bundle — an earlier version of this comment said so
 * and was wrong. KTS.Foundation 0.65.0 ships build/KTS.Foundation.targets with a `KtsBuildTailwind`
 * target, BeforeTargets="Build", conditioned only on this project having a package.json (it does),
 * taking every .razor file here as an input and wwwroot/css/tailwind.css as its output, and running
 * `npm run css:build`. Web/Admin.Web/Dockerfile installs Node 20 for exactly that reason. So a build
 * can and does emit new utilities into the bundle.
 *
 * The real hazard is the one that survives that, and it is why these two rules stay scoped to the
 * component: whether the regeneration runs at all is MSBuild's Inputs/Outputs timestamp comparison,
 * and whether a newly emitted utility ever reaches the deployed app depends on a human noticing and
 * committing the regenerated bundle — a one-line diff in a minified file that nothing gates
 * (css-guard.yml audits *.razor.css, not this artifact). A component-scoped stylesheet is compiled
 * by the Razor SDK from this project's own sources on every build, so it can neither be skipped nor
 * drift from the markup, and css-guard.yml does check it.
 */

/*
 * A long unbroken value — a URL, an imported identifier — must wrap rather than push the cell wide
 * and break the two-column grid.
 */
.resource-field-value[b-kpjsf39s0o] {
    overflow-wrap: anywhere;
}

/*
 * An empty REQUIRED field: a record that needs attention, distinguished from an empty optional one
 * (which is a muted dash) by colour, weight and a triangle-alert glyph, so the distinction does not
 * rest on colour alone.
 *
 * --kts-warning-text, not --kts-warning: the plain warning token is the amber used for FILLS
 * (#f59e0b light) and measures 2.15:1 as ink on the light card. --kts-warning-text is Foundation's
 * warning INK and measures 5.02:1 light (#b45309 on #ffffff) and 6.45:1 dark (#d29922 on #1f2024),
 * both above the 4.5:1 AA floor for body text. --kts-text-faint was rejected for the same reason it
 * was rejected in Pages/Documents.razor.css: 2.54:1 / 3.42:1, below AA in BOTH themes.
 * ResourceDetailEmptyContrastTests computes all of those from the pinned Foundation palette rather
 * than trusting these numbers.
 */
.resource-field-missing[b-kpjsf39s0o] {
    color: var(--kts-warning-text);
}

/*
 * #1272 — the medallion slot on a resource row.
 *
 * The BOX is declared here, not left to the avatar, because the slot has to hold the row's
 * alignment when it is EMPTY: a list whose schema declares a picture field draws a medallion only
 * for the records that have one, and the rest must still indent by the same amount or the list
 * reads as ragged. 2.25rem is KtsAvatar.AvatarSize.Md, whose inline style is `width:36px;height:36px`
 * in the pinned package — the same number, expressed in rem because this rule is ours; the two are
 * held together by ResourceMedallionSlotTests rather than by this sentence.
 *
 * .1875rem is the SAME ring width Pages/Calendar.razor.css gives .doctor-avatar-btn's border,
 * which is the other surface a medic's identity ring is drawn on. One number in two files is a
 * drift risk, so ResourceMedallionMarkupTests reads both and requires them to agree.
 *
 * The ring is a box-shadow and not a border: a border would grow the box, which would move the name
 * for ringed rows only — precisely the raggedness the slot exists to prevent. The colour arrives as
 * --resource-medallion-ring, set inline per row from MedicIdentityColor, and the `transparent`
 * fallback is what a row with no ring gets: the property is simply not set on it, so nothing is
 * painted and nothing has to be undone with a second class.
 */
.resource-medallion[b-kpjsf39s0o] {
    width: 2.25rem;
    height: 2.25rem;
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 9999px;
    box-shadow: 0 0 0 .1875rem var(--resource-medallion-ring, transparent);
}
/* /Pages/Services.razor.rz.scp.css */
/*
 * #1346 — the medallion slot on a service's dependency list.
 *
 * The same box Pages/Resources.razor.css draws, at the size THIS list uses. It cannot simply be
 * shared: Blazor's CSS isolation appends a per-component scope attribute to every element
 * authored in a component's markup, so Resources.razor.css compiles its rule to
 * `.resource-medallion[b-<resources>]` and the span authored here carries `b-<services>` — the
 * rule would match nothing and the medallion would render unsized and unclipped. Putting it in a
 * global stylesheet instead would take a settled #1272 rule out of the isolation it was shipped
 * in. So the rule is restated here, and ServiceMedallionMarkupTests requires the two files to
 * agree on both numbers rather than a comment promising they do.
 *
 * 1.75rem is KtsAvatar.AvatarSize.Sm, whose inline style is `width:28px;height:28px` in the
 * pinned package — this list's rows are chips beside a dependency name, not the primary list
 * rows Resources.razor draws at Md. The test reads the number out of the package by running
 * KtsAvatar's own size resolution, so a Foundation release that resizes Sm fails there rather
 * than silently misaligning the column.
 *
 * The ring width is Resources.razor.css's, unchanged: a medic's identity ring is one visual
 * language across the calendar, the resource list and this list, and retuning one of the three
 * must not leave the others behind.
 *
 * A box-shadow and not a border, for the reason Resources.razor.css states: a border grows the
 * box, which would move the name on ringed rows only — exactly the raggedness the slot exists to
 * prevent. --resource-medallion-ring is simply not set on a medallion with no ring, and the
 * `transparent` fallback is what makes that draw nothing.
 */
.resource-medallion[b-lvc02sn2bj] {
    width: 1.75rem;
    height: 1.75rem;
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 9999px;
    box-shadow: 0 0 0 .1875rem var(--resource-medallion-ring, transparent);
}
