/*
 * Motion and depth tokens.
 *
 * The console is a dense, mostly-flat surface, which is the right call for reading data but leaves
 * every control looking equally inert — nothing announces that it can be pressed, and nothing
 * acknowledges the press. These few tokens give the whole app one vocabulary for lift and timing so
 * the interactions below stay consistent instead of each page inventing its own.
 */
:root {
    --ussd-ease: cubic-bezier(.2, .8, .3, 1);
    --ussd-fast: 120ms;
    --ussd-base: 200ms;
    --ussd-slow: 320ms;

    /* Two-part shadows: a tight contact shadow plus a wide ambient one, so cards sit on the page
       rather than floating over it. */
    --ussd-shadow-rest: 0 1px 2px rgba(0, 0, 0, .06), 0 1px 3px rgba(0, 0, 0, .08);
    --ussd-shadow-hover: 0 2px 4px rgba(0, 0, 0, .08), 0 8px 20px rgba(0, 0, 0, .12);
    --ussd-shadow-raised: 0 4px 8px rgba(0, 0, 0, .1), 0 16px 32px rgba(0, 0, 0, .16);
}

html, body {
    margin: 0;
    font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
}

code {
    font-family: 'JetBrains Mono', 'Cascadia Mono', Consolas, monospace;
    font-size: 0.85em;
}

.mud-table-cell-custom-group {
    font-weight: 600;
    background-color: var(--mud-palette-background-grey);
}

/*
 * FocusOnNavigate moves focus to the page's first <h1> after every navigation, so a screen reader
 * announces where it has landed. To focus a heading it must first be given tabindex="-1", and the
 * browser then paints its focus ring around the wordmark. Keep the focus move, drop the ring: the
 * heading is not interactive, so the outline reads as a rendering fault rather than an affordance.
 */
h1[tabindex="-1"]:focus,
h1[tabindex="-1"]:focus-visible {
    outline: none;
}

pre {
    margin: 0;
    font-family: 'JetBrains Mono', 'Cascadia Mono', Consolas, monospace;
}

/* Sign-in ---------------------------------------------------------------- */

/*
 * The sign-in page is the only screen rendered outside the MudBlazor shell, so it carries its own
 * scale. These tokens keep that self-contained rather than scattering a dozen literal hexes through
 * the rules below; the accent matches PaletteDark.Primary in ConsoleTheme so the console the operator
 * lands on is recognisably the same product.
 *
 * Everything here is on a 4px rhythm and a neutral ramp. A control plane is a place of work: the
 * page earns trust by looking assembled and legible, not by being decorated.
 */
.login-shell {
    --login-bg: #0d0e11;
    --login-surface: #16181d;
    --login-sunken: #101216;
    --login-line: #24272f;
    --login-line-strong: #30343e;
    --login-text: #e8eaed;
    --login-text-muted: #9ba1ab;
    --login-text-faint: #838a95;
    /* Two blues, because they do different jobs. The accent is for thin marks on a dark surface —
       focus rings, the wordmark glyph — where PaletteDark's lighter blue stays legible. The fill is
       the darker Carbon blue used for solid buttons, which is the only one of the two that carries
       white text at 4.5:1. */
    --login-accent: #4589ff;
    --login-fill: #0f62fe;
    --login-fill-hover: #0353e9;
    --login-fill-active: #002d9c;
    --login-danger: #fa4d56;

    box-sizing: border-box;
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 40px 24px;
    background-color: var(--login-bg);
    color: var(--login-text);
    position: relative;
    overflow: hidden;
}

.login-shell *,
.login-shell *::before,
.login-shell *::after {
    box-sizing: inherit;
}

/*
 * Texture instead of colour. A single faint grid, masked to fade before it reaches the edges, gives
 * the empty field around the card some structure without the coloured wash that makes a sign-in page
 * read as a marketing splash. It is decorative, so it stays behind everything and out of the tree.
 */
.login-shell::before {
    content: "";
    position: absolute;
    inset: 0;
    background-image: linear-gradient(rgba(255, 255, 255, .03) 1px, transparent 1px),
                      linear-gradient(90deg, rgba(255, 255, 255, .03) 1px, transparent 1px);
    background-size: 32px 32px;
    -webkit-mask-image: radial-gradient(120% 100% at 50% 0%, #000 0%, transparent 78%);
    mask-image: radial-gradient(120% 100% at 50% 0%, #000 0%, transparent 78%);
    pointer-events: none;
}

.login-panel {
    position: relative;
    width: 100%;
    max-width: 400px;
}

/* Card ------------------------------------------------------------------- */

.login-card {
    padding: 28px;
    border: 1px solid var(--login-line);
    border-radius: 12px;
    background: var(--login-surface);
    box-shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 16px 40px rgba(0, 0, 0, .32);
    animation: ussd-login-in 260ms var(--ussd-ease) both;
}

@keyframes ussd-login-in {
    from { opacity: 0; transform: translateY(6px); }
    to { opacity: 1; transform: none; }
}

.login-title {
    margin: 0 0 24px;
    font-size: 1.25rem;
    font-weight: 600;
    letter-spacing: -.015em;
}

/* Fields ----------------------------------------------------------------- */

.login-field {
    margin-bottom: 16px;
}

.login-label {
    display: block;
    margin-bottom: 6px;
    font-size: .8125rem;
    font-weight: 500;
    color: var(--login-text);
}

.login-control {
    position: relative;
}

.login-input {
    width: 100%;
    height: 42px;
    padding: 0 12px;
    border: 1px solid var(--login-line-strong);
    border-radius: 8px;
    background: var(--login-sunken);
    color: var(--login-text);
    font: inherit;
    font-size: .875rem;
    transition: border-color var(--ussd-fast) var(--ussd-ease),
                box-shadow var(--ussd-fast) var(--ussd-ease),
                background-color var(--ussd-fast) var(--ussd-ease);
}

/* Still 4.5:1 against the sunken field, so the example address is readable rather than implied. */
.login-input::placeholder {
    color: #7b828d;
}

.login-input:hover {
    border-color: #3c414c;
}

.login-input:focus {
    outline: none;
    border-color: var(--login-accent);
    box-shadow: 0 0 0 3px rgba(69, 137, 255, .2);
}

/* The server re-renders with aria-invalid on both fields after a rejected attempt. */
.login-input[aria-invalid="true"] {
    border-color: rgba(250, 77, 86, .55);
}

.login-input[aria-invalid="true"]:focus {
    border-color: var(--login-danger);
    box-shadow: 0 0 0 3px rgba(250, 77, 86, .18);
}

/*
 * Chrome paints autofilled fields with its own near-white background, which on a dark form looks like
 * a rendering fault. The inset shadow is the only way to repaint it; -webkit-text-fill-color is the
 * only way to recolour the text inside it.
 */
.login-input:-webkit-autofill,
.login-input:-webkit-autofill:hover,
.login-input:-webkit-autofill:focus {
    -webkit-text-fill-color: var(--login-text);
    -webkit-box-shadow: 0 0 0 1000px var(--login-sunken) inset;
    caret-color: var(--login-text);
}

/* Room for the reveal control, so a long password never runs underneath it. */
.login-input--secret {
    padding-right: 64px;
}

.login-reveal {
    position: absolute;
    top: 50%;
    right: 6px;
    transform: translateY(-50%);
    padding: 5px 8px;
    border: 0;
    border-radius: 6px;
    background: transparent;
    color: var(--login-text-muted);
    font: inherit;
    font-size: .75rem;
    font-weight: 500;
    cursor: pointer;
    transition: color var(--ussd-fast) var(--ussd-ease),
                background-color var(--ussd-fast) var(--ussd-ease);
}

.login-reveal:hover {
    background: rgba(255, 255, 255, .06);
    color: var(--login-text);
}

.login-reveal:focus-visible {
    outline: 2px solid var(--login-accent);
    outline-offset: 1px;
}

/* Reserves no space when hidden — the hint appears between the field and the button, and the button
   moving under the cursor mid-type would be worse than the hint being missed. */
.login-caps {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 8px 0 0;
    font-size: .75rem;
    color: #f1c21b;
}

.login-caps[hidden] {
    display: none;
}

.login-caps::before {
    content: "";
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: currentColor;
    flex: 0 0 auto;
}

/* Submit ----------------------------------------------------------------- */

.login-submit {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 42px;
    margin-top: 24px;
    padding: 0 12px;
    border: 0;
    border-radius: 8px;
    background: var(--login-fill);
    color: #ffffff;
    font: inherit;
    font-size: .875rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color var(--ussd-fast) var(--ussd-ease);
}

.login-submit:hover {
    background: var(--login-fill-hover);
}

.login-submit:active {
    background: var(--login-fill-active);
}

.login-submit:focus-visible {
    outline: 2px solid #a8c7ff;
    outline-offset: 2px;
}

/* Latched by the inline guard on Login.razor once the form has been submitted once. */
.login-submit.is-busy,
.login-submit:disabled {
    background: var(--login-fill-hover);
    color: rgba(255, 255, 255, .85);
    cursor: progress;
}

.login-spinner {
    box-sizing: border-box;
    width: 0;
    height: 14px;
    border-radius: 50%;
    opacity: 0;
    transition: width var(--ussd-base) var(--ussd-ease),
                margin var(--ussd-base) var(--ussd-ease),
                opacity var(--ussd-fast) linear;
}

.login-submit.is-busy .login-spinner {
    width: 14px;
    margin-inline-end: 8px;
    opacity: 1;
    border: 2px solid rgba(255, 255, 255, .4);
    border-top-color: #ffffff;
    animation: ussd-spin 700ms linear infinite;
}

/* Alert ------------------------------------------------------------------ */

.login-alert {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    margin-bottom: 20px;
    padding: 10px 12px;
    border: 1px solid rgba(250, 77, 86, .4);
    border-radius: 8px;
    background: rgba(250, 77, 86, .1);
    color: #ffb3b8;
    font-size: .8125rem;
    line-height: 1.45;
}

/* Focused by script so the reason is announced; the ring would be noise on a static message. */
.login-alert:focus {
    outline: none;
}

.login-alert__icon {
    width: 15px;
    height: 15px;
    flex: 0 0 auto;
    margin-top: 1px;
    color: var(--login-danger);
}


@media (max-width: 420px) {
    .login-shell {
        padding: 24px 16px;
    }

    .login-card {
        padding: 24px 20px;
    }
}

@media (prefers-reduced-motion: reduce) {
    .login-card {
        animation: none;
    }
}

/* Dashboard ------------------------------------------------------------- */

/*
 * The header was a lone heading at one edge and two controls at the other with nothing between or
 * beneath them, so on a wide screen it read as an empty band rather than as the page's masthead. A
 * hairline rule closes the band and gives the whole page something to hang from; the caption beneath
 * the heading gives the left side enough weight to balance the controls opposite.
 */
.dash-header {
    padding-bottom: 12px;
    border-bottom: 1px solid var(--mud-palette-divider);
}

/* Holds the controls together as one group at the right edge, and keeps them from stretching to the
   full height of the now-taller title block. */
.dash-header__actions {
    flex: 0 0 auto;
    align-self: center;
}

/*
 * A refetch must not move the page. The progress bar gets a permanently reserved slot rather than
 * being inserted into flow, and the content already on screen dims instead of collapsing to a
 * skeleton — the reader keeps their place and the charts keep their frame.
 */
.dash-progress {
    height: 4px;
    margin-bottom: 16px;
}

.dash-body {
    transition: opacity .18s ease;
}

.dash-body.is-refetching {
    opacity: .55;
}

.dash-view-toggle {
    height: auto;
    min-height: 0;
}

/* The table view is the chart's twin, so it lives inside the same fixed-height card. */
.dash-table-scroll {
    max-height: 286px;
    overflow-y: auto;
}

/* Columns of figures align on the digit; the large tile values keep proportional figures. */
.dash-num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* Ranked breakdown ------------------------------------------------------ */

/* Eight rows at this rhythm land near the 286px chart beside them, so the two cards sit level. */
.ranked {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.ranked-head {
    display: flex;
    align-items: baseline;
    gap: 12px;
    margin-bottom: 4px;
}

.ranked-label {
    flex: 1 1 auto;
    min-width: 0;
    font-size: .8125rem;
    color: var(--mud-palette-text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.ranked-figures {
    flex: 0 0 auto;
    display: flex;
    align-items: baseline;
    gap: 8px;
    font-variant-numeric: tabular-nums;
}

.ranked-count {
    font-size: .8125rem;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
}

/* The count is the figure being ranked; the share supports it. */
.ranked-share {
    font-size: .75rem;
    color: var(--mud-palette-text-secondary);
    min-width: 44px;
    text-align: right;
}

/* A neutral track one step off the surface, not a second data colour. */
.ranked-track {
    height: 6px;
    border-radius: 3px;
    background-color: var(--mud-palette-divider);
    overflow: hidden;
}

/*
 * The bar grows from the axis on arrival and eases between widths on a refetch, so a shifting
 * breakdown reads as a change in the data rather than as a redraw. Growth is a scaleX on the
 * composited layer, not an animated width, so eight bars cost no layout work.
 */
.ranked-fill {
    height: 100%;
    border-radius: 3px;
    background-color: var(--mud-palette-primary);
    transform-origin: left center;
    animation: ussd-bar-grow 560ms var(--ussd-ease) both;
    transition: width 480ms var(--ussd-ease);
}

@keyframes ussd-bar-grow {
    from { transform: scaleX(0); }
    to { transform: scaleX(1); }
}

/* Ranked largest-first, so the cascade runs down the ranking. */
.ranked > *:nth-child(1) .ranked-fill { animation-delay: 0ms; }
.ranked > *:nth-child(2) .ranked-fill { animation-delay: 45ms; }
.ranked > *:nth-child(3) .ranked-fill { animation-delay: 90ms; }
.ranked > *:nth-child(4) .ranked-fill { animation-delay: 135ms; }
.ranked > *:nth-child(5) .ranked-fill { animation-delay: 180ms; }
.ranked > *:nth-child(6) .ranked-fill { animation-delay: 225ms; }
.ranked > *:nth-child(7) .ranked-fill { animation-delay: 270ms; }
.ranked > *:nth-child(n+8) .ranked-fill { animation-delay: 315ms; }

/* ApexCharts renders its tooltip as plain DOM outside the SVG, so it needs the app's typeface. */
.apexcharts-tooltip,
.apexcharts-tooltip-title,
.apexcharts-tooltip-text {
    font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif !important;
}

.apexcharts-tooltip {
    border-radius: 8px !important;
    box-shadow: 0 8px 24px rgba(0, 0, 0, .18) !important;
}

/* The tooltip's job is the number; the series name is the secondary element. */
.apexcharts-tooltip-text-y-value {
    font-weight: 600;
}

/* Callback API documentation page */
.ussd-code {
    position: relative;
    background-color: var(--mud-palette-background-grey);
    border: 1px solid var(--mud-palette-divider);
    border-radius: 6px;
    padding: 12px 14px;
    overflow-x: auto;
}

.ussd-code pre {
    font-size: 0.8rem;
    line-height: 1.5;
    white-space: pre;
}

.ussd-code-lang {
    position: absolute;
    top: 6px;
    right: 44px;
    font-size: 0.65rem;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--mud-palette-text-secondary);
}

.ussd-code-copy {
    position: absolute;
    top: 2px;
    right: 4px;
}

/* Busy buttons ---------------------------------------------------------- */

/*
 * A click costs a SignalR round trip before any state changes, and for the first few hundred
 * milliseconds a plain button looks exactly as it did before the press. That gap is where the
 * duplicate submissions come from. Three things close it: the press is acknowledged instantly in
 * CSS (no round trip needed), the button then latches into a spinner, and BusyButton drops the
 * second click server-side even if the operator got one in before the spinner appeared.
 */
.busy-btn__spinner {
    display: inline-block;
    flex: 0 0 auto;
    box-sizing: border-box;
    width: 0;
    height: 1.25rem;
    border-radius: 50%;
    opacity: 0;
    transition: width var(--ussd-base) var(--ussd-ease),
                margin var(--ussd-base) var(--ussd-ease),
                opacity var(--ussd-fast) linear;
}

/*
 * The spinner opens into the slot the start icon just vacated, which is why the icon is dropped in
 * the same render — the label never moves, so the eye stays on the word rather than tracking it.
 */
.busy-btn.is-busy .busy-btn__spinner {
    width: 1.25rem;
    margin-inline-end: 8px;
    opacity: 1;
    border: 2px solid currentColor;
    border-top-color: transparent;
    animation: ussd-spin 700ms linear infinite;
}

/*
 * The button keeps its own colour and label while the request is in flight — it is occupied, not
 * unavailable — and simply stops accepting the pointer. Keyboard activation still reaches the
 * server, where the latch in BusyButton drops it.
 */
.busy-btn.is-busy {
    pointer-events: none;
    cursor: progress;
}

.busy-icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    padding: 12px;
    vertical-align: middle;
}

.busy-icon-btn[data-size="small"] {
    padding: 3px;
}

.busy-icon-btn__spinner {
    box-sizing: border-box;
    width: 1.5rem;
    height: 1.5rem;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    animation: ussd-spin 700ms linear infinite;
}

.busy-icon-btn[data-size="small"] .busy-icon-btn__spinner {
    width: 1.125rem;
    height: 1.125rem;
}

.busy-icon-btn[data-size="large"] .busy-icon-btn__spinner {
    width: 2rem;
    height: 2rem;
}

@keyframes ussd-spin {
    to { transform: rotate(360deg); }
}

/* Press and hover feedback ---------------------------------------------- */

/*
 * The press state is deliberately CSS-only. It fires on pointer-down in the browser, so the operator
 * gets confirmation in the same frame as the click instead of a round trip later — which is the
 * single biggest reason a button gets hit twice.
 */
.mud-button-root {
    transition: background-color var(--ussd-base) var(--ussd-ease),
                box-shadow var(--ussd-base) var(--ussd-ease),
                border-color var(--ussd-base) var(--ussd-ease),
                transform var(--ussd-fast) var(--ussd-ease);
}

.mud-button-filled:not(.mud-button-disabled):hover {
    box-shadow: var(--ussd-shadow-hover);
    transform: translateY(-1px);
}

.mud-button-root:not(.mud-button-disabled):active {
    transform: translateY(1px) scale(.985);
    box-shadow: var(--ussd-shadow-rest);
    transition-duration: 60ms;
}

.mud-icon-button:not(.mud-button-disabled):active {
    transform: scale(.9);
    transition-duration: 60ms;
}

/* A button that is genuinely unavailable should look unavailable rather than merely faint. */
.mud-button-root.mud-button-disabled:not(.is-busy) {
    cursor: not-allowed;
}

/*
 * Keyboard focus was invisible on several controls because MudBlazor leans on the ripple for
 * feedback. A ring on :focus-visible only shows for keyboard traversal, so pointer users never see
 * it.
 */
.mud-button-root:focus-visible,
.mud-nav-link:focus-visible,
.mud-table-row:focus-visible,
.key:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: 2px;
}

/* Surfaces -------------------------------------------------------------- */

/*
 * Every card on the console is Elevation="0", which flattens the page into one plane: filter bars,
 * data cards and the page background all read at the same depth. A hairline border plus a contact
 * shadow separates a card from the page without reintroducing Material's heavy elevation.
 */
/*
 * Deliberately opt-out rather than opt-in: the pages all reach for a bare MudPaper as their card, so
 * matching the element is what keeps this in one place. The exclusions are MudBlazor components that
 * happen to be built on MudPaper and already draw their own frame — an alert, a table, a panel set —
 * where a second border reads as a rendering fault.
 */
.mud-paper.mud-elevation-0:not(.mud-alert):not(.mud-table):not(.mud-expansion-panels):not(.mud-expand-panel):not(.mud-tabs):not(.mud-toolbar):not(.mud-list):not(.mud-card) {
    border: 1px solid var(--mud-palette-divider);
    box-shadow: var(--ussd-shadow-rest);
    transition: box-shadow var(--ussd-base) var(--ussd-ease),
                transform var(--ussd-base) var(--ussd-ease),
                border-color var(--ussd-base) var(--ussd-ease);
}

/* A card nested inside a card is a section of it, not a second object on the page. */
.mud-paper.mud-elevation-0 .mud-paper.mud-elevation-0 {
    border: 0;
    box-shadow: none;
}

/* Stat tiles read as a row of numbers; a leading accent bar gives the row a rhythm and ties each
   tile to the colour of the metric it carries. */
.stat-tile {
    position: relative;
    overflow: hidden;
}

.stat-tile::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: 3px;
    background: var(--stat-accent, var(--mud-palette-primary));
    opacity: .85;
}

.stat-tile__value {
    font-variant-numeric: tabular-nums;
    letter-spacing: -.01em;
}

/* Rows ------------------------------------------------------------------ */

.mud-table-hover .mud-table-row:hover {
    transition: background-color var(--ussd-fast) var(--ussd-ease);
}

/* Clickable rows get the same instant press acknowledgement as buttons. */
.mud-table-row[style*="cursor:pointer"]:active,
.mud-table-hover .mud-table-row:active {
    background-color: var(--mud-palette-action-default-hover);
}

.mud-nav-link {
    transition: background-color var(--ussd-fast) var(--ussd-ease),
                color var(--ussd-fast) var(--ussd-ease),
                padding-inline-start var(--ussd-base) var(--ussd-ease);
}

/* The active page is currently marked by text colour alone, which is easy to lose in a long menu. */
.mud-nav-link.active {
    position: relative;
    background-color: var(--mud-palette-action-default-hover);
}

.mud-nav-link.active::before {
    content: "";
    position: absolute;
    inset: 6px auto 6px 0;
    width: 3px;
    border-radius: 0 3px 3px 0;
    background: var(--mud-palette-primary);
    animation: ussd-nav-mark var(--ussd-base) var(--ussd-ease);
}

@keyframes ussd-nav-mark {
    from { transform: scaleY(0); }
    to { transform: scaleY(1); }
}

/* Entry motion ---------------------------------------------------------- */

/*
 * Navigation currently swaps one full page of content for another between frames, which is what
 * makes the console feel static — nothing ever moves, so nothing signals that something changed.
 * A short rise on the incoming page gives the change a direction without delaying the read.
 */
.page-enter {
    animation: ussd-page-in var(--ussd-slow) var(--ussd-ease) both;
}

@keyframes ussd-page-in {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: none; }
}

/* Applied to grid children so a set of cards arrives as a sequence rather than a block. */
.ussd-stagger > * {
    animation: ussd-page-in var(--ussd-base) var(--ussd-ease) both;
}

.ussd-stagger > *:nth-child(1) { animation-delay: 0ms; }
.ussd-stagger > *:nth-child(2) { animation-delay: 40ms; }
.ussd-stagger > *:nth-child(3) { animation-delay: 80ms; }
.ussd-stagger > *:nth-child(4) { animation-delay: 120ms; }
.ussd-stagger > *:nth-child(5) { animation-delay: 160ms; }
.ussd-stagger > *:nth-child(n+6) { animation-delay: 200ms; }

/* Overlays --------------------------------------------------------------- */

/*
 * MudBlazor cross-fades dialogs and slides drawers, but both start at full size, so a dialog arrives
 * without the sense of having come from anywhere. Adding a small scale to the rise gives the overlay
 * an origin, and — more usefully — makes it obvious that the page underneath is still there.
 */
.mud-dialog {
    animation: ussd-dialog-in 220ms var(--ussd-ease) both;
    box-shadow: var(--ussd-shadow-raised);
}

@keyframes ussd-dialog-in {
    from { opacity: 0; transform: translateY(12px) scale(.97); }
    to { opacity: 1; transform: none; }
}

.mud-overlay .mud-overlay-scrim {
    animation: ussd-fade-in 180ms linear both;
}

@keyframes ussd-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

/* The detail drawers are the console's main reveal; they should ease rather than snap.
   Anchor.End resolves to mud-drawer-pos-right in a left-to-right document. */
.mud-drawer.mud-drawer-pos-right {
    transition: transform var(--ussd-slow) var(--ussd-ease) !important;
    box-shadow: var(--ussd-shadow-raised);
}

/* Drawer content trails the panel slightly, so the panel reads as the container and the content as
   what it brought with it. */
.mud-drawer.mud-drawer-pos-right.mud-drawer--open > * {
    animation: ussd-page-in var(--ussd-base) var(--ussd-ease) 80ms both;
}

.mud-snackbar {
    animation: ussd-snackbar-in 260ms var(--ussd-ease) both;
    box-shadow: var(--ussd-shadow-hover);
}

@keyframes ussd-snackbar-in {
    from { opacity: 0; transform: translateX(16px) scale(.98); }
    to { opacity: 1; transform: none; }
}

.mud-tooltip {
    animation: ussd-tooltip-in 140ms var(--ussd-ease) both;
}

@keyframes ussd-tooltip-in {
    from { opacity: 0; transform: translateY(4px) scale(.96); }
    to { opacity: 1; transform: none; }
}

/* Expansion panels are the trace viewer's main control; the chevron should turn with the panel. */
.mud-expand-panel .mud-expand-panel-header .mud-icon-root {
    transition: transform var(--ussd-base) var(--ussd-ease);
}

.mud-expand-panel .mud-expand-panel-header:hover {
    background-color: var(--mud-palette-action-default-hover);
}

/* Loading ---------------------------------------------------------------- */

/*
 * An indeterminate bar says "something is happening" but not "your data is on its way". A shimmer
 * over the shape the content will take does both, and because the layout is already correct the
 * arrival costs no reflow.
 */
.ussd-skeleton {
    position: relative;
    overflow: hidden;
    border-radius: 6px;
    background-color: var(--mud-palette-action-default-hover);
}

.ussd-skeleton::after {
    content: "";
    position: absolute;
    inset: 0;
    transform: translateX(-100%);
    background: linear-gradient(90deg,
        transparent 0%,
        var(--mud-palette-action-disabled-background) 50%,
        transparent 100%);
    animation: ussd-shimmer 1.4s var(--ussd-ease) infinite;
}

@keyframes ussd-shimmer {
    to { transform: translateX(100%); }
}

/*
 * The dashboard dims its previous render while refetching. Pairing the dim with a barely-there
 * scale reads as "this is being replaced" rather than "this has gone faulty".
 */
.dash-body.is-refetching {
    transform: scale(.997);
}

.mud-progress-linear {
    overflow: hidden;
    border-radius: 999px;
}

/* Live values ------------------------------------------------------------ */

/*
 * Fired by re-keying the figure in StatTile, so it only ever runs when the number actually changed.
 * The lift is small and the flash brief: on a dashboard that polls, anything louder becomes noise
 * within a minute.
 */
.stat-tile__figure {
    display: inline-block;
    animation: ussd-figure-in 420ms var(--ussd-ease) both;
}

@keyframes ussd-figure-in {
    from { opacity: 0; transform: translateY(-6px); }
    60% { opacity: 1; }
    to { opacity: 1; transform: none; }
}

/* Rows arriving from a fetch cascade instead of appearing as a block. Capped at ten so a large
   result set does not spend a second unfolding. */
.ussd-rows-in > tr {
    animation: ussd-row-in var(--ussd-base) var(--ussd-ease) both;
}

.ussd-rows-in > tr:nth-child(1) { animation-delay: 0ms; }
.ussd-rows-in > tr:nth-child(2) { animation-delay: 20ms; }
.ussd-rows-in > tr:nth-child(3) { animation-delay: 40ms; }
.ussd-rows-in > tr:nth-child(4) { animation-delay: 60ms; }
.ussd-rows-in > tr:nth-child(5) { animation-delay: 80ms; }
.ussd-rows-in > tr:nth-child(6) { animation-delay: 100ms; }
.ussd-rows-in > tr:nth-child(7) { animation-delay: 120ms; }
.ussd-rows-in > tr:nth-child(8) { animation-delay: 140ms; }
.ussd-rows-in > tr:nth-child(9) { animation-delay: 160ms; }
.ussd-rows-in > tr:nth-child(n+10) { animation-delay: 180ms; }

@keyframes ussd-row-in {
    from { opacity: 0; transform: translateY(4px); }
    to { opacity: 1; transform: none; }
}

/* Chips carry status; a state change should be visible without a page-level refresh cue. */
.mud-chip {
    transition: background-color var(--ussd-base) var(--ussd-ease),
                border-color var(--ussd-base) var(--ussd-ease),
                color var(--ussd-base) var(--ussd-ease),
                transform var(--ussd-fast) var(--ussd-ease);
}

.mud-chip.mud-clickable:hover {
    transform: translateY(-1px);
}


/* Chips carrying live state (a running session, a health verdict) get a slow pulse so the page
   reads as connected rather than as a screenshot. */
.ussd-live-dot .mud-chip-icon {
    animation: ussd-pulse 2s var(--ussd-ease) infinite;
}

@keyframes ussd-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: .35; }
}

/*
 * Honour the OS setting. Everything above is decoration; none of it carries information that is
 * lost when the motion is removed, so it can all be cut without a fallback — except the spinners,
 * which are the only signal that a request is in flight and so keep turning, slowly.
 */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
        scroll-behavior: auto !important;
    }

    .busy-btn.is-busy .busy-btn__spinner,
    .busy-icon-btn__spinner {
        animation: ussd-spin 1.4s linear infinite !important;
        animation-duration: 1.4s !important;
        animation-iteration-count: infinite !important;
    }

    .mud-button-filled:not(.mud-button-disabled):hover,
    .mud-button-root:not(.mud-button-disabled):active,
    .mud-icon-button:not(.mud-button-disabled):active,
    .mud-chip.mud-clickable:hover,
    .dash-body.is-refetching {
        transform: none;
    }
}
