/*
   Autonomycs design system - the primitives every Autonomycs web app shares.

   Class names in this file are a PUBLISHED CONTRACT: My.Web (and later Admin.Web and the docs
   site) reference them from markup, so renaming one is a breaking change across every consumer.

   Rules keep the source order they had in the MY portal's app.css they were extracted from - the
   cascade was audited once for that order, and reordering re-rolls those dice. App-specific
   styling (shell chrome, screen-specific families) stays in each app's own stylesheet, which
   loads AFTER this one.

   The .rz-* rules at the bottom skin Radzen components onto the brand. They are inert unless the
   consumer links Radzen's material-base.css BEFORE this file; this library deliberately takes no
   Radzen package dependency.
*/

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

html,
body {
    margin: 0;
    padding: 0;
}

body {
    background: var(--canvas);
    font-family: var(--font);
    color: var(--ink);
    -webkit-font-smoothing: antialiased;
}

/* FocusOnNavigate (App.razor) moves focus to the page's h1 after every navigation so assistive
   tech announces the new page - and the browser then paints its focus ring on a heading nobody
   can tab to. Keep the focus, drop the ring. */
h1:focus {
    outline: none;
}

a {
    color: var(--brand);
    text-decoration: none;
}

a:hover {
    color: var(--accent);
}

input,
select,
button,
textarea {
    font-family: inherit;
}

h1 {
    margin: 0;
    font: 500 38px/1.1 var(--font);
    letter-spacing: -.02em;
}

h2 {
    margin: 0;
    font: 500 24px/1.2 var(--font);
}

/* -------------------------------------------------- boot splash
   Shown by index.html before the runtime downloads, and replaced by the app. A WebAssembly cold
   start is seconds rather than milliseconds, so this is a real screen the user sees, not a
   formality. */
.app-boot {
    position: fixed;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 28px;
    background: var(--brand);

    /* The mark inside is drawn with fill="currentColor" - see BrandMark.razor - so this is what
       makes it white rather than the body's ink. */
    color: #fff;
}

.app-boot__mark {
    width: 64px;
    height: auto;
    display: block;
}

.app-boot__bar {
    width: 180px;
    height: 4px;
    border-radius: 999px;
    background: rgba(255, 255, 255, .22);
    overflow: hidden;
}

.app-boot__bar span {
    display: block;
    width: 40%;
    height: 100%;
    border-radius: 999px;
    background: #fff;
    animation: app-boot-slide 1.1s ease-in-out infinite;
}

@keyframes app-boot-slide {
    0% { transform: translateX(-100%); }
    100% { transform: translateX(350%); }
}

.page {
    max-width: 1220px;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: 24px;
}

.page__lead {
    margin: 0;
    font: 400 18px/1.5 var(--font);
    color: var(--muted);
}

.page__title {
    margin: 0 0 6px;
}

/* Two-column content. auto-fit rather than a fixed pair, so the sidebar column drops beneath the
   main one before the cards themselves get too narrow to read. */
.split {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
    gap: 24px;
    align-items: start;
}

.stack {
    display: flex;
    flex-direction: column;
    gap: 24px;
}

/* Self-contained, NOT a modifier that needs .stack alongside it. Every use site applies it on its
   own, and while it only set `gap` that did nothing at all: without display:flex there is no gap, so
   the home page's cards sat flush against each other and its document links - being inline elements -
   ran together on one line. */
.stack--tight {
    display: flex;
    flex-direction: column;
    gap: 20px;
}

.langpicker {
    position: relative;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 9px 14px;
    background: var(--canvas);
    border-radius: var(--radius-control);
    cursor: pointer;
}

.langpicker__label {
    font: 500 14px/1 var(--font);
    color: var(--muted);
}

/* The Radzen dropdown, blended into the pill: the pill is the control's chrome, so the control
   itself loses its own. The open panel keeps the theme, which is already mapped to the brand
   tokens at the bottom of this file.

   The hover/focus selectors are not optional: the theme's own hover rule ADDS a border at higher
   specificity, which grew the control by a pixel and made the whole top bar jump on mouse-over. */
.langpicker .rz-dropdown,
.langpicker .rz-dropdown:not(.rz-state-disabled):hover,
.langpicker .rz-dropdown:not(.rz-state-disabled):focus-within,
.langpicker .rz-dropdown.rz-state-focused {
    border: 0;
    border-block-end: 0;
    background: transparent;
    box-shadow: none;
    height: auto;
    min-height: 0;
}

/* Wide enough for "Nederlands" so the pill does not resize per language. A separate rule, NOT part
   of the state group above: the mobile override below must be able to win from the base width
   without also having to out-rank the hover/focus selectors. */
.langpicker .rz-dropdown {
    min-width: 118px;
}

.langpicker .rz-dropdown .rz-dropdown-label {
    padding: 0;
    font: 500 15px/1.2 var(--font);
    color: var(--ink);
}

.langpicker .rz-dropdown .rzi {
    color: var(--muted);
}

/* -------------------------------------------------- cards */
.card {
    background: #fff;
    border: 1px solid var(--card-border);
    border-radius: var(--radius-card);
    padding: 28px;
}

.card--tight {
    padding: 24px;
}

.card--list {
    padding: 12px 28px 20px;
}

.card__head {
    display: flex;
    flex-wrap: wrap;
    gap: 8px 16px;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 20px;
}

.card__eyebrow {
    font: 500 13px/1 var(--font);
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: .1em;
}

.card__link {
    font: 500 16px/1 var(--font);
}

.alert {
    background: var(--surface-orange);
    border: 1px solid var(--accent);
    border-radius: var(--radius-card);
    padding: 24px;
}

.alert__title {
    margin: 0;
    font: 500 19px/1.3 var(--font);
}

.alert__body {
    margin: 8px 0 18px;
    font: 400 16px/1.45 var(--font);
    color: var(--muted);
}

/* -------------------------------------------------- rows */
.row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 16px 20px;
    padding: 18px 0;
    border-bottom: 1px solid var(--hairline);
}

.row:last-child {
    border-bottom: 0;
}

.row--topline {
    padding: 20px 0;
    gap: 14px 18px;
    border-bottom: 0;
    border-top: 1px solid var(--hairline);
}

.row__main {
    flex: 1 1 190px;
    min-width: 190px;
}

.row__title {
    margin: 0;
    font: 500 19px/1.3 var(--font);
}

.row__meta {
    margin: 2px 0 0;
    font: 400 16px/1.4 var(--font);
    color: var(--muted);
}

.row__button {
    background: transparent;
    color: var(--brand);
    padding: 12px 18px;
    border-radius: var(--radius-control);
    font: 500 15px/1 var(--font);
    cursor: pointer;
    border: 1px solid var(--hairline-strong);
}

.row__button:hover {
    background: var(--canvas);
}

.row__button--primary {
    border-color: var(--brand);
}

.row__button--primary:hover {
    background: var(--brand);
    color: #fff;
}

.dot {
    width: 10px;
    height: 10px;
    flex: none;
    border-radius: 50%;
    background: var(--olive);
}

.dot--next {
    background: var(--accent);
}

.dot--measurement {
    background: var(--green-text);
}

/* -------------------------------------------------- badges, buttons */
.badge {
    padding: 7px 13px;
    border-radius: 999px;
    background: var(--canvas);
    color: var(--deep);
    font: 500 14px/1 var(--font);
    white-space: nowrap;
}

.badge--measurement {
    background: var(--surface-green);
    color: var(--green-text);
}

/* A cancelled place. Muted rather than struck through: the row is still information the member wants
   to read, it just is not something they are attending. */
.badge--cancelled {
    background: var(--surface-orange);
    color: var(--rust);
}

.badge--next {
    background: var(--surface-orange);
    color: var(--rust);
    padding: 8px 14px;
}

.btn {
    border: 1px solid var(--hairline-strong);
    background: transparent;
    color: var(--brand);
    padding: 16px 24px;
    border-radius: 13px;
    font: 500 17px/1 var(--font);
    cursor: pointer;
}

.btn:hover:not(:disabled) {
    background: var(--canvas);
}

.btn:disabled {
    opacity: .45;
    cursor: not-allowed;
}

.btn--primary {
    border: 0;
    background: var(--accent);
    color: #fff;
}

.btn--primary:hover:not(:disabled) {
    background: #e96a15;
    color: #fff;
}

/*
    The third weight: .btn's treatment, laid out for an icon beside its label - the admin portal's
    top-bar sign-out, and the externals' sign-in button on both staff apps. Moved here from
    Autonomycs.Admin.Web's app.css when the measure app became the second consumer, which is the rule
    that file's own comment set.

    It must stay AFTER .btn: the two have equal specificity, so .btn--ghost wins its layout only by
    source order. Alphabetising it above .btn silently reverts the ghost to block layout.

    A consumer that wants only the bordered weight, with no icon, can use plain .btn - the border,
    background and colour here are .btn's own, re-stated so the modifier reads as a complete look.
*/
.btn--ghost {
    border: 1px solid var(--hairline-strong);
    background: transparent;
    color: var(--brand);
    display: inline-flex;
    align-items: center;
    gap: 10px;
}

.btn--ghost:hover {
    background: var(--canvas);
}

.btn--block {
    width: 100%;
    border: 0;
    background: var(--accent);
    color: #fff;
    padding: 15px;
    border-radius: var(--radius-control);
    font: 500 16px/1 var(--font);
    cursor: pointer;
}

.btn--block:disabled {
    opacity: .5;
    cursor: not-allowed;
}

.tabs {
    display: flex;
    gap: 6px;
    padding: 16px 0 12px;
    flex-wrap: wrap;
}

.tab {
    padding: 10px 18px;
    border: 0;
    border-radius: 999px;
    background: var(--canvas);
    color: var(--muted);
    font: 500 15px/1 var(--font);
    cursor: pointer;
}

.tab--active {
    background: var(--brand);
    color: #fff;
}

/*
    ValueBandBar: a Radzen chart, skinned. One stacked column on its 0..max scale with the normal
    band's bounds across it, at a fixed height so a row of items lines up.

    THE COLOURS LIVE HERE rather than on the component's attributes, because this project's palette is
    tokens.css and C# in this assembly cannot read it - Ui.Web references nothing, BrandColors
    included. material-base.css is linked before this file, so an equal-specificity rule here wins on
    order alone.
*/
.vbar {
    /* Wide enough that the value axis on the left does not squeeze the plot into a sliver: the
       numbers take about a third of it, and what is left is the column's room. */
    width: 176px;
    height: 200px;
    justify-self: end;

    /* Radzen's chart variables, which is how the axis, its numbers and the gridlines are reached:
       they are drawn as SVG with no class worth targeting per instance. */
    --rz-chart-axis-color: var(--border);
    --rz-chart-axis-label-color: var(--muted);
    --rz-chart-grid-line-color: var(--border);
}

/* The chart's own default box is 300px wide. */
.vbar .rz-chart {
    width: 100%;
    height: 100%;
}

/*
    The column's colour, set two ways on purpose.

    --rz-series-1 is the real mechanism: the theme's own rule for the first series reads it
    (`.rz-scheme-pastel .rz-series-item-0 { fill: var(--rz-series-1) }`), so feeding the variable
    colours whatever element Radzen decides to paint, without a specificity fight and whatever the
    class on it turns out to be.

    The rule below it is the belt to that brace, for a colour scheme that hardcodes its hexes instead
    of reading the variable: same specificity as the theme's, and this file is linked after
    material-base.css, so it wins on order.

    It is NOT widened to descendants, however tempting. The band's reference lines are children of the
    series and carry no class of their own, so a `.rz-column-series *` would paint them the bar's
    colour and lose the band.
*/
.vbar--good {
    --rz-series-1: var(--green-text);
    --vbar-colour: var(--green-text);
}

.vbar--low {
    --rz-series-1: var(--rust);
    --vbar-colour: var(--rust);
}

.vbar--neutral {
    --rz-series-1: var(--muted);
    --vbar-colour: var(--muted);
}

.vbar .rz-series-item-0 {
    fill: var(--vbar-colour);
    stroke: var(--vbar-colour);
}

/*
    The value above the column. It sits inside the series group, so without this it inherits the bar's
    colour and a green number lands on a green bar the moment the bar is tall enough to reach it.
*/
.vbar .rz-series-data-label,
.vbar .rz-series-data-label-fill {
    fill: var(--ink);
    stroke: none;
    font: 500 13px/1 var(--font);
}

.consentbody {
    font: 400 17px/1.6 var(--font);
    color: var(--ink);
    overflow-wrap: break-word;
}

.consentbody p {
    margin: 0 0 16px;
}

.consentbody h1,
.consentbody h2,
.consentbody h3 {
    margin: 20px 0 10px;
    font: 500 20px/1.3 var(--font);
    letter-spacing: 0;
}

.consentbody ul,
.consentbody ol {
    margin: 0 0 16px;
    padding-left: 24px;
}

.consentbody a {
    text-decoration: underline;
}

/* -------------------------------------------------- tables (payments, results)
   CSS-grid tables per the design; the column template comes from a per-table class. Below tablet
   width each row collapses into a two-column label-free stack. */
.table__head {
    display: grid;
    gap: 16px;
    padding: 20px 0 14px;
    border-bottom: 1px solid rgba(106, 97, 29, .16);
    font: 500 14px/1 var(--font);
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: .08em;
}

.table__row {
    display: grid;
    gap: 16px;
    padding: 18px 0;
    border-bottom: 1px solid rgba(106, 97, 29, .1);
    align-items: center;
    font: 400 17px/1.3 var(--font);
}

.table__row:last-child {
    border-bottom: 0;
}

.table__strong {
    font: 500 19px/1.2 var(--font);
}

.table__muted {
    color: var(--muted);
}

@media (max-width: 700px) {
    .table__head {
        display: none;
    }

    .table__row {
        grid-template-columns: 1fr auto;
    }
}

.field {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.field__label {
    font: 500 16px/1 var(--font);
    color: var(--deep);
}

.field__input {
    padding: 16px 18px;
    border: 1px solid rgba(106, 97, 29, .28);
    border-radius: var(--radius-control);
    font: 400 18px/1.2 var(--font);
    background: #fff;
    color: var(--ink);
}

.field__error {
    font: 400 14px/1.3 var(--font);
    color: var(--rust);
}

/* A Radzen dropdown dressed as a .field__input. The border colour lives in a custom property so
   the invalid state only swaps the variable - the hover/focus rules below (needed because the
   theme's own hover rule adds a different border at higher specificity) then inherit it. */
.field .rz-dropdown.fieldselect {
    --fieldselect-border: rgba(106, 97, 29, .28);

    width: 100%;
}

.field .rz-dropdown.fieldselect,
.field .rz-dropdown.fieldselect:not(.rz-state-disabled):hover,
.field .rz-dropdown.fieldselect:not(.rz-state-disabled):focus-within,
.field .rz-dropdown.fieldselect.rz-state-focused {
    padding: 16px 18px;
    border: 1px solid var(--fieldselect-border);
    border-radius: var(--radius-control);
    background: #fff;
    box-shadow: none;
    height: auto;
    min-height: 0;
}

.fieldselect .rz-dropdown-label {
    padding: 0;
    font: 400 18px/1.2 var(--font);
    color: var(--ink);
}

.fieldselect .rzi {
    color: var(--muted);
}

.fieldselect .rz-placeholder {
    color: var(--muted);
}

.field .rz-dropdown.fieldselect.field__input--invalid {
    --fieldselect-border: var(--rust);
}

.field__input:disabled,
.field__input[readonly] {
    background: var(--surface-neutral);
    color: var(--muted);
    cursor: not-allowed;
    border-color: rgba(106, 97, 29, .18);
}

/* -------------------------------------------------- filter pills
   The design's two pill variants: filled on the results picker, outlined on the documents filters.
   Both share the active state. */
.pillrow {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

.pill {
    border: 0;
    background: var(--canvas);
    padding: 9px 15px;
    border-radius: 999px;
    font: 500 14px/1 var(--font);
    color: var(--muted);
    cursor: pointer;
}

.pill:hover {
    background: var(--border);
}

.pill--outline {
    border: 1px solid rgba(106, 97, 29, .18);
    background: #fff;
    padding: 11px 19px;
    font-size: 15px;
}

.pill--outline:hover {
    background: var(--canvas);
}

.pill--active,
.pill--outline.pill--active {
    background: var(--brand);
    border-color: var(--brand);
    color: #fff;
}

/* -------------------------------------------------- page head with an action
   Title/lead on the left, a primary action on the right, wrapping below tablet width. */
.page__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 16px 24px;
    flex-wrap: wrap;
}

/* Sub-heading inside a list card, per the design's vouchers/payments sections. */
.card__subhead {
    margin: 24px 0 6px;
    font: 500 24px/1.2 var(--font);
}

.card__note {
    margin: 0 0 8px;
    font: 400 16px/1.4 var(--font);
    color: var(--muted);
}

.loadmore {
    display: flex;
    justify-content: center;
    padding: 18px 0 6px;
}

/* -------------------------------------------------- detail pane */
.detail__head {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: flex-start;
    gap: 16px 20px;
    margin-bottom: 24px;
}

.detail__title {
    margin: 0 0 6px;
    font: 500 26px/1.15 var(--font);
}

.detail__sub {
    margin: 0;
    font: 400 17px/1.4 var(--font);
    color: var(--muted);
}

.facts {
    display: flex;
    flex-direction: column;
    margin-bottom: 26px;
}

.fact {
    display: flex;
    justify-content: space-between;
    gap: 16px;
    padding: 15px 0;
    border-bottom: 1px solid var(--hairline);
}

.fact:last-child {
    border-bottom: 0;
}

.fact__label {
    font: 400 17px/1.3 var(--font);
    color: var(--muted);
}

.fact__value {
    font: 500 17px/1.3 var(--font);
    text-align: right;
}

.actions {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
}

.footnote {
    margin: 18px 0 0;
    font: 400 15px/1.5 var(--font);
    color: var(--muted);
}

.empty {
    margin: 8px 0;
    font: 400 17px/1.5 var(--font);
    color: var(--muted);
}

/* -------------------------------------------------- states */
.spinner {
    display: flex;
    justify-content: center;
    padding: 26px 0;
}

.spinner::after {
    content: "";
    width: 34px;
    height: 34px;
    border-radius: 50%;
    border: 3px solid var(--border);
    border-top-color: var(--brand);
    animation: spinner-turn .8s linear infinite;
}

@keyframes spinner-turn {
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .spinner::after {
        animation-duration: 2.4s;
    }
}

.skeleton {
    background: linear-gradient(90deg, var(--surface-neutral) 25%, var(--border) 37%, var(--surface-neutral) 63%);
    background-size: 400% 100%;
    animation: skeleton 1.4s ease infinite;
    border-radius: 10px;
    height: 18px;
}

@keyframes skeleton {
    0% { background-position: 100% 50%; }
    100% { background-position: 0 50%; }
}

.loadfail {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 14px;
    padding: 18px 20px;
    border: 1px solid var(--accent);
    background: var(--surface-orange);
    border-radius: 14px;
    font: 400 16px/1.4 var(--font);
}

/* -------------------------------------------------- blazor error bar */
#blazor-error-ui {
    background: var(--surface-orange);
    border-top: 1px solid var(--accent);
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, .1);
    display: none;
    left: 0;
    padding: 14px 20px 14px 20px;
    position: fixed;
    width: 100%;
    z-index: 1000;
    font: 400 16px/1.4 var(--font);
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 18px;
    top: 12px;
}

@media (max-width: 560px) {
    h1 {
        font-size: 30px;
    }

    .card,
    .card--list {
        padding: 20px;
    }

    /* The language pill drops its "Taal" label - the dropdown's own value says what it is. */
    .langpicker {
        padding: 9px 10px;
    }

    .langpicker__label {
        display: none;
    }

    .langpicker .rz-dropdown {
        min-width: 104px;
    }

    .fact {
        flex-direction: column;
        gap: 2px;
    }

    .fact__value {
        text-align: left;
    }
}

/* -------------------------------------------------- signed-out screen
   The design's login artboard, minus its email and password fields — B2C owns credentials, and a
   second set of fields here would either be decorative or collect what this app must not see. */

.signedout {
    max-width: 1100px;
    width: 100%;
    margin: auto;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    border-radius: 24px;
    overflow: hidden;
    border: 1px solid var(--card-border);
    min-height: 560px;
}

.signedout__panel {
    background: #fff;
    padding: 52px 48px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 24px;
}

.signedout__panel--brand {
    background: var(--brand);
    color: #fff;
    padding: 44px;
    justify-content: space-between;
}

.signedout__mark {
    width: 44px;
    height: auto;
    display: block;
}

.signedout__claim {
    margin: 0 0 12px;
    font: 500 34px/1.15 var(--font);
    color: #fff;
    letter-spacing: -.02em;
}

.signedout__blurb {
    margin: 0;
    font: 400 18px/1.55 var(--font);
    color: rgba(255, 255, 255, .82);
    max-width: 380px;
}

.signedout__title {
    margin: 0 0 8px;
    font: 500 30px/1.15 var(--font);
    letter-spacing: 0;
}

.signedout__title--minor {
    font-size: 22px;
}

.signedout__sub {
    margin: 0;
    font: 400 17px/1.5 var(--font);
    color: var(--muted);
}

/* -------------------------------------------------- Radzen
   Map Radzen's theme variables onto the brand tokens rather than restyling components one at a
   time — a component added later then inherits the brand instead of arriving in Material purple. */
:root {
    --rz-primary: var(--brand);
    --rz-primary-light: var(--olive);
    --rz-secondary: var(--accent);
    --rz-text-color: var(--ink);
    --rz-text-secondary-color: var(--muted);
    --rz-base-background-color: var(--canvas);
    --rz-body-background-color: var(--canvas);
    --rz-body-color: var(--ink);
    --rz-border-radius: var(--radius-control);
    --rz-text-font-family: var(--font);
    --rz-danger: var(--red);
    --rz-warning: var(--accent);
    --rz-success: var(--green-text);
}

.rz-dialog {
    border-radius: 22px;
    font-family: var(--font);
}

/*
    A DIALOG NEVER FILLS THE VIEWPORT. Radzen's own frame is max-height: 100% and takes whatever
    width it is given, so a dialog asked for 100vw, or one whose content grows, reaches the edges
    and stops reading as a dialog. This caps every dialog at --dialog-inset from each edge; the
    frame is overflow: hidden and .rz-dialog-content scrolls, so a capped dialog scrolls its body
    rather than the page. A dialog that wants to be as large as allowed asks for 100vw / 100vh and
    lets this rule inset it, so the number lives in one place.

    From 769px up only, and that breakpoint is Radzen's: at 768px and below it turns every
    non-alert dialog into a full-width sheet pinned to the top, which is right on a phone and what
    the MY portal relies on. A cap there would leave a narrowed sheet with square corners.
*/
@media (min-width: 769px) {
    .rz-dialog {
        max-width: calc(100vw - 2 * var(--dialog-inset));
        max-height: calc(100vh - 2 * var(--dialog-inset));
    }
}

/*
    THE CHARTS DIALOG KEEPS ONE OPEN ANIMATION, WHATEVER IS INSIDE IT.

    Radzen animates a dialog open with opacity AND scale, except when it contains a chart:

        .rz-dialog.rz-open              { animation: ...opacity, ...scale }
        .rz-dialog:has(.rz-chart).rz-open { animation: ...opacity }

    Sensible on its own - scaling a chart's SVG as it appears looks bad. But :has() is LIVE, and this
    dialog swaps measure items in place: step from an item with a chart to one that is only a value
    and .rz-chart leaves the DOM, the shorthand changes, and the browser RESTARTS the animation. The
    dialog visibly shrinks to half and grows back - 0.1s, which reads as it closing and reopening.
    Only in that direction, because the other way round merely re-runs the opacity fade.

    So this pins the same animation for both states. Equal specificity to Radzen's rule (three
    classes), and this file is linked after material-base.css, so it wins on order - the same
    arrangement every other .rz-* rule here relies on.
*/
.rz-dialog:has(.chartdialog).rz-open {
    animation: var(--rz-dialog-transition) rz-dialog-open-opacity;
}

.rz-dialog-titlebar {
    border-radius: 22px 22px 0 0;
}

/*
    A dropdown is clicked, closed and open alike, so it shows the hand. Radzen sets no cursor on the
    closed control - a form dropdown inherited the arrow from its label - and says cursor: default
    on its option rows outright. The :not() lifts specificity past those rows' own rules, so this
    wins whatever the order.
*/
.rz-dropdown:not(.rz-state-disabled),
.rz-dropdown-items li:not(.rz-state-disabled),
.rz-dropdown-item:not(.rz-state-disabled),
.rz-multiselect-items li:not(.rz-state-disabled) {
    cursor: pointer;
}

.rz-notification-item {
    font-family: var(--font);

    /* The severity colour is painted by an inner element whose corners poke past the item's own
       radius, leaving white triangles. Matching radius + clipping fixes both corners. */
    border-radius: var(--radius-control);
    overflow: hidden;
}

/* The item sits inside a wrapper that paints its OWN white rectangle, which showed as square
   corners behind the rounded item above. The item carries the visible surface; the wrapper is
   layout only. */
/*
    Radzen draws the close control as `content: "close"` on a ::before, relying on the Material
    Symbols LIGATURE font to turn that word into a cross. The rule above puts our own font on the
    notification, the button inherits it, ES Rebond Grotesque has no such ligature - and the glyph
    comes out as tofu.

    So the font is handed back to the button explicitly. Only the button: the message text beside it
    stays in the brand font, which is the reason the rule above exists at all.
*/
.rz-notification-close {
    font-family: var(--rz-icon-font-family, "Material Symbols");
}

.rz-notification-item-wrapper {
    background: transparent;
}

/* -------------------------------------------------- stat tile

   One number and its label, for a dashboard's summary row. Tone drives the value's colour: the
   admin design brief's seventh friction is a dashboard where every card weighed the same, so the
   whole point of this component is that a figure someone is waiting on does not look like a figure
   that is merely true. */
/* The row a dashboard puts them in. auto-fit rather than a fixed count, so four tiles sit in a
   row on a wide screen and reflow to two on a narrow one without the page choosing a breakpoint. */
.stattiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px;
}
.stattile {
    background: #fff;
    border: 1px solid var(--card-border);
    border-radius: var(--radius-card-sm);
    padding: 20px 22px;
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.stattile__label {
    font: 400 15px/1.35 var(--font);
    color: var(--muted);
}
/*
    The number and what qualifies it, side by side while they fit.

    Wrapping matters because the unit is two different lengths of thing: "/ 12" sits beside a figure
    happily, and "96 betalingen" beside "€ 18.406" does not - and the euro sign is glued to its
    amount, so the value cannot give way. Without the wrap the unit simply left the card.
*/
.stattile__figure {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 9px;
    min-width: 0;
}
.stattile__value {
    font: 500 34px/1 var(--font);
    letter-spacing: -.02em;
    /* Tabular figures so a row of tiles does not jitter as the numbers land. */
    font-variant-numeric: tabular-nums;
}
.stattile__value--neutral {
    color: var(--ink);
}
.stattile__value--attention {
    color: var(--accent);
}
/* --rust rather than --accent: it is the darker orange that keeps contrast against a tint, and it
   is what the artboards use for the figure someone is actually waiting on. */
.stattile__value--urgent {
    color: var(--rust);
}
.stattile__unit {
    min-width: 0;
    font: 400 15px/1.2 var(--font);
    color: var(--muted-soft);
}

/* -------------------------------------------------- status pill

   A state, named. The word is always rendered and the tint is the second signal - the admin design
   brief's fifth friction was status as a bare coloured dot, which is unreadable without the legend
   and says nothing at all to anyone who cannot separate the hues.

   The tints are the surface tokens rather than the status tokens: --success and friends are
   full-strength and made for an icon or a border, and a row of them at pill size turns a table into
   a traffic light. The text carries the saturated colour instead. */
.statuspill {
    display: inline-flex;
    align-items: baseline;
    gap: 5px;
    padding: 5px 11px;
    border-radius: 999px;
    font: 500 14px/1.2 var(--font);
    white-space: nowrap;
}
.statuspill__reason {
    font-weight: 400;
    opacity: .8;
}
/*
    The neutral pill carries a border, and the other three do not.

    Its background is --canvas, which is the page's own colour: on a card or in a dialog that is very
    nearly white on white, so "Overgeslagen" read as bare text beside a "Verzonden" that was clearly
    a chip. The tinted tones have enough contrast to draw their own edge; this one does not, and a
    pill that only looks like a pill on some surfaces is not a pill.
*/
.statuspill--neutral {
    background: var(--canvas);
    color: var(--deep);
    box-shadow: inset 0 0 0 1px var(--hairline-strong);
}
.statuspill--good {
    background: var(--surface-green);
    color: var(--green-text);
}
.statuspill--warning {
    background: var(--surface-orange);
    color: var(--rust);
}
.statuspill--bad {
    background: var(--surface-neutral);
    color: var(--red);
}

/* -------------------------------------------------- week planner

   WeekPlanner: Radzen's scheduler, skinned into the brand's week grid. The component hides Radzen's
   own header and draws a day header of its own above the grid, so the rules here are in two families:
   .planner__* for what this design system draws, and .planner .rz-* for what Radzen draws.

   THE COLOURS GO THROUGH RADZEN'S OWN VARIABLES where one exists, as .vbar does for the chart - so a
   Radzen restyle that moves a property still reads the brand from the same place. Only what has no
   variable is set as a property. */
.planner {
    /* The hours column, and the one value both the header row and the grid size it from. */
    --planner-hours-w: 76px;

    /* The pitch and the day count. The component writes all three inline (HourHeightPx,
       60 / MinutesPerSlot, 7 or 1); these are the values a bare .planner falls back to. */
    --planner-hour: 72px;
    --planner-slots: 2;
    --planner-days: 7;

    --rz-scheduler-border-color: var(--hairline);
    --rz-scheduler-minor-border-color: var(--hairline);
    --rz-scheduler-background-color: #fff;
    --rz-scheduler-color: var(--ink);
    --rz-scheduler-event-background-color: #fff;
    --rz-scheduler-event-color: var(--ink);
    --rz-scheduler-event-content-padding: 6px 9px;
    --rz-scheduler-event-font-size: 14px;
    --rz-scheduler-event-line-height: 1.25;
    --rz-scheduler-event-padding-block: 0 3px;
    --rz-scheduler-event-padding-inline: 0 3px;
    --rz-scheduler-border-radius: 8px;
    --rz-scheduler-shadow: none;
    --rz-scheduler-focus-outline: 2px solid var(--brand);

    /* Seven days need room; below this the card that holds the planner scrolls sideways rather than
       the columns crushing the cards to a sliver. */
    min-width: 940px;
    font-family: var(--font);
}

/* -- the header row this component draws -- */
.planner__head {
    display: grid;
    grid-template-columns: var(--planner-hours-w) repeat(var(--planner-days), minmax(0, 1fr));
    border-bottom: 1px solid var(--card-border);
    background: var(--surface-neutral);
}

/* One day needs no minimum; the min-width is for seven columns of cards. */
.planner--day,
.planner--agenda {
    min-width: 0;
}

.planner__day {
    padding: 12px 10px;
    text-align: center;
    border-left: 1px solid var(--hairline);
}

/*
    Today is marked with a rule and the brand colour on the number rather than a fill: a fill would
    compete with the tints inside the column, which are the ones carrying meaning.
*/
.planner__day--today {
    box-shadow: inset 0 -3px 0 var(--brand);
}

.planner__dow {
    display: block;
    font: 500 13px/1.2 var(--font);
    color: var(--muted-soft);
    text-transform: uppercase;
    letter-spacing: .08em;
}

.planner__num {
    display: block;
    margin-top: 5px;
    font: 500 22px/1 var(--font);
    font-variant-numeric: tabular-nums;
}

.planner__day--today .planner__num {
    color: var(--brand);
}

.planner__count {
    display: block;
    margin-top: 5px;
    font: 400 13px/1.2 var(--font);
    color: var(--muted-soft);
}

/* -- Radzen's grid, made to lie flat on the page --

   Radzen draws a 400px box that scrolls inside itself. The page is the scroll container here, so the
   box loses its height and its border, and its scroll box (.rz-view-content, which is height: 0 and
   overflow: auto - the one that actually collapses the grid if only the outer height is changed)
   becomes as tall as its hours. Its own header goes: the component draws one above. */
.planner .rz-scheduler {
    height: auto;
    border: 0;
    border-radius: 0;
    overflow: visible;
    background: transparent;
}

.planner .rz-view-content {
    height: auto;
    overflow: visible;
}

.planner .rz-view-header {
    display: none;
}

.planner .rz-slot-hours {
    flex: 0 0 var(--planner-hours-w);
}

/*
    THE PITCH. Radzen sizes a slot at 1.5em and places an event at 1.5em per slot, so the height of an
    hour is a font-size on the slot column: --planner-hour over 1.5 slots' worth. Slot titles and
    event text are sized by Radzen's absolute variables, so they do not grow with it.

    The hours column is the trap: Radzen sizes ITS rows in rem, not em, so changing this alone leaves
    the labels drifting down the column. The rule after this one keeps them on the hour lines.
*/
.planner .rz-slots {
    font-size: calc(var(--planner-hour) / (1.5 * var(--planner-slots)));
}

.planner .rz-slot-hours .rz-slot-header {
    height: calc(var(--planner-hour) / var(--planner-slots));
    padding: 6px 10px 0 0;
    border: 0;
    text-align: right;
    font: 400 14px/1 var(--font);
    color: var(--muted-soft);
    font-variant-numeric: tabular-nums;
}

.planner .rz-slot {
    cursor: pointer;
}

.planner .rz-slot:hover {
    background: var(--surface-neutral);
}

/* The half-hour line, lighter than the hour's without a token of its own. */
.planner .rz-slot-minor {
    border-block-start-style: dashed;
}

/* Never give .rz-slot a position: the cards paint over the slots only because .rz-events is
   positioned and the slots are not. This is the belt to that braces. */
.planner .rz-events {
    z-index: 1;
}

.planner__slot--today {
    background: var(--surface-neutral);
}

/* The grid's positioning box. The now line is absolute inside it, so it must be positioned and
   nothing between it and the line may be: on .planner itself the line would measure from the top of
   the header row, whose height is the consumer's. */
.planner__grid {
    position: relative;
}

/*
    THE NOW LINE. Radzen draws none (ShowTimeIndicator is Syncfusion's), so the component lays one
    over today's column. --planner-now-day is that column, zero-based, and --planner-now-min the
    minutes below the first row, both written inline by the component; the pitch is the same
    --planner-hour the slots are sized from, so the line and the cards agree on where a minute is.
    The columns are Radzen's flex: 1 day columns after the hours gutter - the arithmetic the header
    row mirrors as a grid. Above the cards (.rz-events is 1) and inert to the pointer, so a click
    under it still reaches the slot or the card.
*/
.planner__now {
    position: absolute;
    left: calc(var(--planner-hours-w) + (100% - var(--planner-hours-w)) * var(--planner-now-day) / var(--planner-days));
    width: calc((100% - var(--planner-hours-w)) / var(--planner-days));
    top: calc(var(--planner-hour) * var(--planner-now-min) / 60);
    height: 0;
    border-top: 2px solid var(--red);
    z-index: 2;
    pointer-events: none;
}

/* The dot at the column's left edge, centred on the line. */
.planner__now::before {
    content: "";
    position: absolute;
    left: -4px;
    top: -5px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--red);
}

/*
    THE CARD SHELL. Single-class on purpose: the component adds planner__item to every card through
    Radzen's render hook, and a consumer's own single-class state modifier - loaded after this file -
    wins on order alone, without knowing that Radzen is underneath. Fill, colour, padding and radius
    come from the variables above; this is the frame.
*/
.planner__item {
    border: 1px solid var(--card-border);
    border-left: 3px solid var(--olive);
    text-align: left;
    cursor: pointer;
}

.planner .rz-event:hover .planner__item,
.planner .rz-event:focus-visible .planner__item {
    border-color: var(--brand);
}

/* The card is the focusable element (Radzen makes it a button); the ring sits just outside it. */
.planner .rz-event:focus-visible {
    outline: var(--rz-scheduler-focus-outline);
    outline-offset: 1px;
    border-radius: var(--rz-scheduler-border-radius);
}

/* -- the agenda: days as rows, each with its date beside it --

   Radzen lays a day out as a header (the number and the day name) beside a slot holding that day's
   cards in flow, so nothing here is positioned and the pitch does not apply. The date column takes
   the width the hour gutter has in the grid views, which keeps the cards' left edge where the eye
   already expects it when switching. */
.planner .rz-view-content-agenda {
    background: #fff;
}

.planner .rz-view-content-agenda > .rz-stack {
    border-bottom: 1px solid var(--hairline);
    padding: 12px 0;
}

.planner .rz-slot-header-agenda {
    width: var(--planner-hours-w);
    min-width: var(--planner-hours-w);
    margin-top: 0;
    padding: 6px 10px 0 0;
}

.planner .rz-slot-header-agenda .rz-slot-header-agenda-day-number {
    font: 500 22px/1 var(--font);
    color: var(--ink);
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.planner .rz-slot-header-agenda .rz-slot-header-agenda-day-title {
    margin-top: 5px;
    font: 500 13px/1.2 var(--font);
    color: var(--muted-soft);
    text-align: right;
    text-transform: uppercase;
    letter-spacing: .08em;
}

.planner .rz-slot-agenda {
    width: auto;
    min-width: 0;
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding-right: 10px;
    cursor: pointer;
}

/* An agenda card is in flow: give it the height its content needs and the whole row's width. */
.planner .rz-event-agenda {
    position: static;
    width: auto;
}

.planner .rz-event-agenda .rz-event-content {
    height: auto;
}

/* Radzen's own time column on an agenda row is off (ShowTimeDisplay=false): the card's template
   already says when, and two clocks in one row read as two things. */
.planner .rz-event-agenda-time {
    display: none;
}

/* -------------------------------------------------- results: MeasurementValues, SessionDeviceResults

   Extracted from the MY portal's Results page when the admin's member Analyse started drawing the
   same thing. Appended here rather than beside the rows and tables they build on: rule order in
   this file is load-bearing, and a new family at the end ties with nothing. */

/* A measure item as a member's result: text on the left, its ValueBandBar on the right. */
.measureitem {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 18px 28px;
    align-items: start;
    padding: 22px 0;
    border-bottom: 1px dashed rgba(106, 97, 29, .25);
}

.measureitem:last-child {
    border-bottom: 0;
}

.measureitem__desc {
    margin-top: 6px;

    /* The descriptions are authored with line breaks that JSON preserves and HTML collapses;
       without this every paragraph runs together on one line. */
    white-space: pre-line;
}

/* Why an item was not measured. Rust rather than muted: it is a gap somebody chose to leave, which
   is the one line in the list a trainer needs to notice. */
.measureitem__omitted {
    color: var(--rust);
}

/* Per-device results table. Nine columns is genuinely wide; the card scrolls it rather than the
   page, per the repo's own rule that wide content scrolls in its own container. */
.devicecard__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    margin-bottom: 4px;
}

.devicecard__name {
    flex: 1;
}

.devicetable {
    overflow-x: auto;
}

.resultrow {
    grid-template-columns: 70px 80px repeat(5, minmax(96px, 1fr)) 80px 90px;
    min-width: 900px;
}

/* The device's picture. A sized box so a missing or slow image cannot reflow the card, and
   object-fit keeps arbitrary uploads presentable. */
.deviceimage {
    width: 56px;
    height: 56px;
    object-fit: cover;
    border-radius: 12px;
    border: 1px solid var(--border);
    flex: none;
}

@media (max-width: 700px) {
    .measureitem {
        grid-template-columns: 1fr;
    }

    /* Stacked under its text rather than hugging the right edge - centred reads as "this chart
       belongs to the item above", where right-aligned read as clutter. Set on the side column now
       that the bar is a flex item within it rather than a grid item of the row; the bare .vbar rule
       (justify-self: end) sits earlier in this file and no longer applies to it. */
    .measureitem__side {
        align-items: center;
    }

    /* The session-results grid is 900px wide and only scrolled on a phone, hiding the header row
       off-screen - so the data meant nothing. Stack instead: the header row disappears and every
       cell draws its own column label (data-label, set in the markup), label left, value right. */
    .resultrow.table__head {
        display: none;
    }

    .table__row.resultrow {
        display: flex;
        flex-direction: column;
        align-items: stretch;
        gap: 8px;
        min-width: 0;
    }

    .table__row.resultrow > span {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: 16px;
    }

    .table__row.resultrow > span::before {
        content: attr(data-label);
        font: 400 14px/1.4 var(--font);
        color: var(--muted);
    }
}

/* The event strip above a result: a scrolling row of cards, one per session or measurement, the
   selected one tinted. The MY Results page's picker, and the admin's member Analyse is laid out
   the same way on purpose - the two screens show the same thing and should read the same. */
.resultstrip {
    display: flex;
    gap: 12px;
    overflow-x: auto;
    padding-bottom: 4px;
    scrollbar-width: thin;
}

.resultcard {
    flex: 0 0 210px;
    text-align: left;
    border: 1px solid var(--card-border);
    background: #fff;
    border-radius: 14px;
    padding: 15px 16px;
    cursor: pointer;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    font: inherit;
    color: inherit;
}

.resultcard:hover {
    border-color: var(--olive);
}

.resultcard--selected {
    background: var(--canvas);
    border-color: var(--brand);
}

/* A card about something that was undone - a session the member was taken off. Kept, because it
   is a fact about the history, and drawn back so it does not read as one they attended. */
.resultcard--muted .resultcard__name,
.resultcard--muted .resultcard__meta {
    color: var(--muted-soft);
}

.resultcard .dot {
    margin-bottom: 10px;
}

.resultcard--selected .dot {
    background: var(--accent);
}

.resultcard__name {
    font: 500 17px/1.3 var(--font);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

.resultcard__meta {
    font: 400 15px/1.4 var(--font);
    color: var(--muted);
}

/* -------------------------------------------------- reschedule dialog
   Day chips across the top, the free slots for the chosen day below. Written for the MY portal's
   RescheduleDialog and moved here when the admin's RescheduleMemberDialog became its second
   consumer: a trainer moving a member and the member moving themselves look at the same rows. */
.slotdays {
    display: flex;
    gap: 8px;
    margin: 4px 0 18px;
    flex-wrap: wrap;
}

.slotday {
    border: 0;
    background: var(--canvas);
    padding: 12px 19px;
    border-radius: 999px;
    cursor: pointer;
    font: 500 15px/1 var(--font);
    color: var(--muted);
}

.slotday:hover {
    background: var(--border);
}

.slotday--active {
    background: var(--brand);
    color: #fff;
}

/* Fixed height, deliberately. The number of free slots differs per day, so sizing to content made
   the dialog jump every time a day chip was tapped - and the chips themselves move under the pointer
   as it grows, which is how you end up selecting a day you did not mean to. Overflow scrolls instead.
   Sized to show four slots, which is what the artboards draw. */
.slotlist {
    display: flex;
    flex-direction: column;
    gap: 10px;
    height: 340px;
    overflow-y: auto;
    padding-right: 4px;
}

.slot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 14px 18px;

    /* In a fixed-height flex column the default is to shrink children to fit, so a day with eight
       slots would squash them all instead of scrolling. */
    flex: none;
    border: 1px solid var(--border);
    background: #fff;
    border-radius: 14px;
    padding: 16px 18px;
}

.slot--last {
    border-color: var(--accent);
    background: var(--surface-orange);
}

.slot__time {
    margin: 0;
    font: 500 18px/1.2 var(--font);
}

.slot__meta {
    margin: 4px 0 0;
    font: 400 16px/1.3 var(--font);
    color: var(--muted);
}

.slot--last .slot__meta {
    color: var(--rust);
}

.slot__pick {
    border: 0;
    background: var(--brand);
    color: #fff;
    padding: 13px 20px;
    border-radius: var(--radius-control);
    font: 500 16px/1 var(--font);
    cursor: pointer;
}

.slot--last .slot__pick {
    background: var(--accent);
}

/* -------------------------------------------------- dialog forms, and the switch dialog

   Moved here from the MY portal when the company portal's change-plan dialog needed the same rows:
   one formula (or group) per row, drawn as the slots above, which is why these follow .slot. */
.stopdialog {
    display: flex;
    flex-direction: column;
    gap: 20px;
}

/* -------------------------------------------------- switch dialog: one formula per row, as the reschedule slots */
.switchdialog__options {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.switchopt {
    width: 100%;
    text-align: left;
    cursor: pointer;
    font: inherit;
    color: inherit;
    justify-content: space-between;
}

.switchopt__main {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.switchopt--picked {
    border-color: var(--brand);
    box-shadow: inset 0 0 0 1px var(--brand);
}

.switchopt--current {
    cursor: default;
    opacity: .6;
}

.switchopt:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

.switchdialog__note {
    margin: 0;
    font: 400 15px/1.5 var(--font);
    color: var(--muted);
}

/* The second step: the groups to leave or join, under their own label. */
.switchdialog__groups {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.switchdialog__groups .field__label {
    margin: 0;
}

.switchdialog__note--warn {
    color: var(--rust);
}

/* The done step's heading: the one line in the dialog that is not muted. */
.switchdialog__donetitle {
    margin: 0;
    font: 500 24px/1.2 var(--font);
}

/* -------------------------------------------------- measure item charts: the row button and the dialog

   Appended at the end for the reason the result families were: rule order in this file is
   load-bearing, and a new family here ties with nothing. */

/*
    The row's right-hand side: the chart button, then the bar.

    A column of its own rather than a third grid track, so a row WITHOUT a bar still puts its button
    in the same corner - with two tracks the button would slide left into the space the bar was
    going to take. `align-items: start` on .measureitem is what lines the button's top up with the
    item's name and value on the left.
*/
.measureitem__side {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 12px;
}

/* Lighter than the base .btn but still a real target - it was small enough to read as a caption
   rather than as something to press. The row's subject is still the value, so it stays quieter than
   a primary action. */
.measureitem__chart {
    padding: 11px 20px;
    font-size: 16px;
    flex: none;
}

/*
    The charts dialog: the measurement's items down the left, one item's charts on the right.

    `auto` rather than a width, because the first column is EMPTY for a single item - a list with one
    entry is a choice with nothing to choose - and an empty element measures zero, so the main column
    then takes the whole dialog and nothing looks reserved. The list itself sets its own width.
*/
.chartdialog {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: stretch;

    /*
        EACH COLUMN SCROLLS, THIS DOES NOT - the .notedialog__grid arrangement, and not the nested
        scrollbars this file warns about elsewhere: the dialog body around it stops scrolling, and its
        two children take over.

        It fixes two things that were one problem. A long list used to push the chart off the top as
        you scrolled down it, because only the page scrolled; and every switch of item resized the
        whole scrolling area, so the content jumped and the dialog read as closing and reopening.
        Now the frame never moves and only the pane you are looking at does.

        Belt and braces on the height: `flex` is what the admin's flex .dialog__body answers to,
        `height` what MY's block body answers to. Where neither applies - the docs site, in flow -
        both are inert and the thing simply sizes to its content.
    */
    flex: 1 1 auto;
    height: 100%;
    min-height: 0;
    overflow: hidden;
}

/*
    The gap belongs to the sidebar, not to the grid.

    An empty element measures zero but a COLUMN GAP does not: with the gap on .chartdialog the
    reserved column pushed everything 24px right of the dialog's own title, which reads as the
    content being indented for no reason. So it is spent only once there is something in that column
    to be separated from - and the day the measure item list lands, it comes back on its own.
*/
.chartdialog:has(.chartdialog__aside:not(:empty)) {
    column-gap: 24px;
}

.chartdialog__main {
    min-width: 0;

    /* min-height is what lets a grid item shrink below its content - without it it reports its full
       height and nothing inside it ever scrolls. The scrolling itself is one level down. */
    min-height: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

/*
    The charts column's scroller, and there is a NEW ONE PER ITEM - the element is keyed on the
    measure item id.

    Scrolling here rather than on the column is what stops the pane jolting. One scroller shared
    across items keeps its scrollTop, so stepping off a long trend somebody had scrolled down left
    the browser to clamp that scroll back to zero as the content shrank - a jump, and only ever in
    that direction. A fresh element is at the top already.
*/
.chartdialog__scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;

    /* The gutter is held whether or not this item's chart needs it. Without it a tall item takes a
       scrollbar, the column narrows, and the chart inside re-measures and redraws - a flash on the
       one element that cannot afford one. */
    scrollbar-gutter: stable;
}

/*
    The head holds the view picker and nothing else.

    It used to name the item and print its value beside the dropdown, and both were removed: the
    dialog's own title bar already carries the name, and the value is the thing the chart is drawing -
    stating it again above the chart says nothing the chart does not.

    Left-aligned for that reason. With the title gone there is nothing for the control to sit
    opposite, and a lone control pinned right reads as detached from what it governs.
*/
.chartdialog__head {
    display: flex;
    align-items: flex-end;
    gap: 16px 24px;
    flex-wrap: wrap;
    padding-bottom: 14px;
    border-bottom: 1px dashed rgba(106, 97, 29, .25);

    /*
        THE HEIGHT IS RESERVED, and this is what stops the pane flashing.

        An item with no chart at all draws neither picker nor view name, so this used to collapse to
        nothing - and everything below it jumped 55px up and back down on every step from a bar item
        to a value-only one, which is what read as the dialog reopening. 55px is the picker's own
        measured height; border-box so the number is the whole box rather than the box plus padding.
    */
    box-sizing: border-box;
    min-height: 55px;
}

/* Reserved, but not drawn: a rule with nothing above it is a divider dividing nothing. The space
   stays, the line goes. This only ever happens with the list up - a row's own chart button appears
   for a LIJN item, which always has something in the head. */
.chartdialog__head:empty {
    border-bottom-color: transparent;
}

/* The picker keeps its own width rather than growing with the head: a dropdown as wide as the dialog
   reads as a search box. */
.chartdialog__picker {
    flex: none;
    min-width: 180px;
}

/* What is drawn when there is only one view - the name as text, never a dropdown holding one option,
   which reads as a control that does not work. Sized as a field's value so it lines up with the
   dropdown it stands in for. */
.chartdialog__only {
    flex: none;
    margin: 0;
    padding-bottom: 11px;
    color: var(--muted);
    font: 500 15px/1 var(--font);
}

.chartdialog__view {
    padding-top: 20px;
}

/*
    Busy and failed, held at a chart's own height.

    Switching measure item swaps one of these in while the next series is fetched. Left to size
    themselves they are a fraction of a chart's height, so the dialog collapsed and sprang back on
    every click of the list - which reads as the dialog closing and reopening rather than as loading.
*/
.chartdialog__state {
    display: grid;
    place-items: center;
    min-height: 380px;
}

/* The bar keeps the size it has on a row - a chart that changed shape when opened would read as a
   different measurement - so it is centred in the space rather than stretched across it. A column,
   because the caption sits under it. */
.chartdialog__bar {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
}

/* When the value was taken, and from which measurement. The same weight as a trend row's own date,
   so the two views read alike one dropdown apart. */
.chartdialog__when {
    margin: 0;
    color: var(--muted);
    font: 400 14px/1.4 var(--font);
    text-align: center;
}

/* An item with no chart at all: its value is the whole answer, so it is given the room a chart would
   have had rather than being tucked into a corner of it. */
.chartdialog__plain {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-height: 200px;
}

.chartdialog__plainvalue {
    margin: 0;
    font: 500 28px/1.2 var(--font);
}

/* -------------------------------------------------- the measure item list

   STICKY RATHER THAN SEPARATELY SCROLLABLE. The dialog body around this already scrolls; a second
   scrolling region inside it is the failure mode this file warns about at .slotlist and the admin's
   .notedialog__list. Sticky keeps the list in view while a long trend scrolls past, and adds no
   second scrollbar. `top: 0` measures against that scrolling body. */
/*
    The list's column, with the rule that separates it from the charts.

    It scrolls itself rather than riding the dialog's scroll: twenty onderdelen are taller than the
    dialog, and reaching the last of them used to take the chart off the top of the screen with it.
    Sticky was the first answer and only half of one - it kept the list in view and let the chart
    leave. The border spans the column, which is why the column is stretched.
*/
.chartdialog__aside:not(:empty) {
    border-right: 1px solid var(--border);
    padding-right: 24px;
    min-height: 0;
    overflow-y: auto;
    scrollbar-width: thin;
}

.itemnav {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin: 0;
    padding: 0;
    list-style: none;

    /* A cap, because the grid column is `auto`: without it a name like "30 sec chair stand test"
       stretches the list across half the dialog and squeezes the chart into what is left. */
    width: 220px;
}

.itemnav__row {
    display: flex;
}

/* The selectable-row treatment is .resultcard's, which is the strip above this dialog on both
   screens - so the list reads as the same kind of control rather than as a new one. */
.itemnav__item {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    text-align: left;
    border: 1px solid transparent;
    background: transparent;
    border-radius: var(--radius-control);
    padding: 10px 12px;
    cursor: pointer;
    font: inherit;
    color: inherit;
}

.itemnav__item:hover {
    background: var(--canvas);
}

.itemnav__item--selected {
    background: var(--canvas);
    border-color: var(--brand);
}

/* One line each, cut with an ellipsis: the list is a column of fixed width, and a name that wrapped
   to three lines would make neighbouring entries different heights for no reason. */
.itemnav__name,
.itemnav__value {
    max-width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.itemnav__name {
    font: 500 15px/1.3 var(--font);
}

.itemnav__value {
    font: 400 14px/1.3 var(--font);
    color: var(--muted);
}

/* -------------------------------------------------- the trend chart */

.trendchart {
    display: grid;
    gap: 20px;
}

/*
    The plot's box.

    A HEIGHT IS REQUIRED, not merely tidy: RadzenChart renders into a box it is given and collapses to
    nothing inside a grid track that sizes to content. The width is 100% so the chart follows the
    dialog rather than Radzen's own 300px default.
*/
.trendchart__plot {
    width: 100%;
    height: 320px;

    /* Radzen's chart variables - the axis, its numbers and the gridlines are SVG with no class worth
       targeting per instance, exactly as in .vbar. */
    --rz-chart-axis-color: var(--border);
    --rz-chart-axis-label-color: var(--muted);
    --rz-chart-grid-line-color: var(--border);
}

.trendchart__plot .rz-chart {
    width: 100%;
    height: 100%;
}

/* The markers on the line. Radzen paints them from the series colour it resolved itself, so this only
   has to give them a ring to sit in - a filled circle on a line of the same colour disappears into it.
   #fff rather than a token because there is none for a surface: .card writes the same literal. */
.trendchart__plot .rz-marker {
    stroke: #fff;
    stroke-width: 2;
}

.trendchart--empty {
    display: grid;
    place-items: center;
    min-height: 200px;
}

/* -------------------------------------------------- the trend in words */

.trendsummary {
    display: grid;
    gap: 14px;
}

.trendsummary__overall {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 16px;
    padding: 14px 16px;
    border-radius: var(--radius-control);
    background: var(--surface-neutral);
}

.trendsummary__label {
    color: var(--muted);
    font: 500 14px/1 var(--font);
}

.trendsummary__value {
    font: 500 20px/1 var(--font);
}

.trendsteps {
    margin: 0;
    padding: 0;
    list-style: none;
}

.trendstep {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    gap: 16px;
    align-items: baseline;
    padding: 10px 0;
    border-bottom: 1px dashed rgba(106, 97, 29, .25);
}

.trendstep:last-child {
    border-bottom: 0;
}

.trendstep__when {
    color: var(--muted);
    font: 400 14px/1.4 var(--font);
}

.trendstep__value {
    font: 500 15px/1 var(--font);
}

.trendstep__change {
    min-width: 72px;
    text-align: right;
    font: 500 15px/1 var(--font);
}

/*
    The direction, coloured the way ValueBandBar already colours a value: green for the way the member
    wants to go, rust for the other. The rule that decides which is MeasureItemTrend.Tone, not this
    file - these three classes only paint what it answered.
*/
.trend--good .trendstep__change,
.trend--good .trendsummary__value {
    color: var(--green-text);
}

.trend--poor .trendstep__change,
.trend--poor .trendsummary__value {
    color: var(--rust);
}

.trend--flat .trendstep__change,
.trend--flat .trendsummary__value {
    color: var(--muted);
}

@media (max-width: 700px) {
    /* One column, and the empty aside would otherwise hold a gap open.

       The internal scrolling goes with it: two stacked scroll areas in a phone's height leaves each
       of them a few centimetres, and the dialog is a full-width sheet that scrolls perfectly well
       as one thing. */
    .chartdialog {
        grid-template-columns: 1fr;
        gap: 0;
        height: auto;
        overflow: visible;
    }

    .chartdialog__main,
    .chartdialog__scroll {
        overflow: visible;
    }

    .chartdialog__aside:empty {
        display: none;
    }

    /* Stacked above the chart at this width, so the list goes sideways instead: fifteen names down a
       phone would push the chart clean off the screen. The shape is .resultstrip's, which is the row
       of cards already sitting above this dialog on both screens. Sticky is dropped with it - it
       would pin the strip over the chart as the body scrolls. */
    /* A horizontal strip has nothing to be separated from sideways, and the stretch would stand a
       full-height rule beside one row of chips. */
    .chartdialog__aside:not(:empty) {
        border-right: 0;
        padding-right: 0;
        margin-bottom: 16px;
        overflow-y: visible;
    }

    .itemnav {
        flex-direction: row;
        width: auto;
        gap: 8px;
        overflow-x: auto;
        padding-bottom: 4px;
        scrollbar-width: thin;
    }

    .itemnav__row {
        flex: 0 0 auto;
    }

    .itemnav__item {
        border-color: var(--card-border);
        background: #fff;
    }

    .trendchart__plot {
        height: 260px;
    }

    /* Three columns in a phone's width leaves the date on two lines and the value on one character.
       The change keeps the right edge; when and what share the line above it. */
    .trendstep {
        grid-template-columns: minmax(0, 1fr) auto;
    }

    .trendstep__when {
        grid-column: 1 / -1;
    }
}

/* -------------------------------------------------- rate trend chart

   RateTrendChart: a share per period on a 0-100 % scale. The line and its markers are coloured by
   the component (a line series is painted from attributes Radzen resolves itself); the box, the axis
   text and the three axis lines are here. */
.ratetrend {
    /* The height itself is the component's Height parameter, written inline: RadzenChart renders
       into the box it is given and collapses to nothing inside a track that sizes to content. */
    width: 100%;

    /* Transparent hides the vertical axis line and every tick mark; the category axis line is given
       its colour back below, because it is the chart's bottom line. */
    --rz-chart-axis-color: transparent;
    --rz-chart-axis-label-color: var(--muted);
    --rz-chart-axis-font-size: 13px;
    --rz-chart-grid-line-color: var(--chart-grey);
}

.ratetrend .rz-chart {
    width: 100%;
    height: 100%;
}

.ratetrend .rz-category-axis .rz-line {
    stroke: var(--chart-grey);
}

/* The dashed middle line. The axis always has three ticks and Radzen draws no gridline on the bottom
   one, so the middle is the only gridline that is not the last path in the axis group. */
.ratetrend .rz-value-axis .rz-grid-line:not(:last-of-type) {
    stroke-dasharray: 5 5;
}

/* The design's line has rounded corners where it turns. A property, not an attribute, so it reaches
   the path whatever Radzen writes on it. */
.ratetrend .rz-line-series path {
    stroke-linejoin: round;
    stroke-linecap: round;
}

.ratetrend--empty {
    display: grid;
    place-items: center;
}

/* -------------------------------------------------- rate bars

   RateBars: a label and its percentage above a horizontal bar, one row per item. CSS rather than a
   chart - see "Charts: when Radzen, when CSS" in this project's CLAUDE.md. Every fill is the brand
   colour; a row below the list's average is olive (RateBar.BelowAverage decides which). */
.ratebars {
    display: grid;
    gap: 20px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.ratebar__head {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 6px 16px;
    margin-bottom: 8px;
    font: 500 17px/1.25 var(--font);
    color: var(--ink);
}

.ratebar__label {
    min-width: 0;
    overflow-wrap: anywhere;
}

.ratebar__value {
    font-variant-numeric: tabular-nums;
}

/* The gap is a margin, not a space in the markup: Razor drops the whitespace between the percentage
   and this span, and "82%· 41 van 50" is what came out. */
.ratebar__sub {
    margin-left: .3em;
    color: var(--muted);
    font-weight: 400;
}

.ratebar__track {
    height: 14px;
    border-radius: 999px;
    background: var(--chart-grey);
    overflow: hidden;
}

.ratebar__fill {
    display: block;
    height: 100%;
    background: var(--brand);
}

.ratebar__fill--below {
    background: var(--olive);
}

/* -------------------------------------------------- row action: a square icon button on a list row

   Moved here from the admin portal's app.css when the company portal's employee rows became its second
   consumer. Outlined until hovered; --primary is the solid one, --danger the destructive one. The
   disabled rules come last: they undo the hover tints, and --danger's must follow the plain one it
   ties with on specificity. */
.dashrow__action {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    border: 1px solid var(--hairline-strong);
    border-radius: 10px;
    background: #fff;
    color: var(--brand);
    cursor: pointer;
}

.dashrow__action:hover {
    background: var(--brand);
    color: #fff;
}

.dashrow__action--primary {
    border-color: var(--brand);
    background: var(--brand);
    color: #fff;
}

.dashrow__action--primary:hover {
    background: var(--deep);
    border-color: var(--deep);
}

/* The destructive controls on this screen - off the waiting list, and throwing away unlinked device
   data - and the only place --error is used on the dashboard. Outlined until hover: a row of solid
   red buttons down a list reads as an alarm rather than as an option. */
.dashrow__action--danger {
    border-color: var(--error);
    color: var(--error);
}

.dashrow__action--danger:hover {
    background: var(--error);
    border-color: var(--error);
    color: #fff;
}

/* A row action is disabled for two quite different reasons - transiently while a write is in flight,
   and standingly where the row does not allow it (the sessions tab's delete, greyed while anybody is
   still enrolled). Both want the same thing: it must LOOK unavailable. Without this it kept its full
   red outline and simply did nothing when clicked, which is the failure "disabled rather than hidden"
   is chosen to avoid - the tooltip carrying the reason is only reachable on hover. The hover tints
   have to be undone as well, or a greyed button still lights up under the pointer. */
.dashrow__action:disabled {
    opacity: .4;
    cursor: default;
}

.dashrow__action:disabled:hover {
    background: #fff;
    color: var(--brand);
}

/* After the rule above, which it ties with on specificity: a danger button keeps its own colour. */
.dashrow__action--danger:disabled:hover {
    background: #fff;
    color: var(--error);
}
