﻿/* ==========================================================================
   Foundations
   Typography, numerals, focus and motion. These apply everywhere, so they are
   the cheapest place to raise the floor across all screens at once.
   ========================================================================== */

html {
    /* Stops iOS Safari from inflating text in landscape, which silently breaks
       carefully set type scales on phones. */
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}

body {
    /* The system UI font is designed for its own rasteriser; these two settings are
       what make it look intentional rather than default. */
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    /* Real apostrophes, proper ligatures — free polish from the font itself. */
    font-feature-settings: "kern" 1, "liga" 1;
}

/* Figures that sit in columns must line up. Proportional digits make a table of
   amounts or scores look ragged and slow it down to read — the operator's eye has
   to re-find the decimal on every row. */
.mud-table-cell,
.mud-table-cell *,
.mud-data-grid .mud-table-cell,
.mud-chip,
code, kbd, samp, pre {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
}

/* Long opaque strings — object ids, hashes, tokens, fingerprints — are compared
   character by character, not read as words. A monospaced face makes that possible
   and stops an l/1 or O/0 mix-up from becoming an incident. */
code, kbd, samp, pre {
    font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
                 "Liberation Mono", monospace;
}

/* Keyboard focus must be visible. MudBlazor's default relies on a subtle ripple that
   never appears for keyboard users, which leaves the app unusable without a mouse.
   :focus-visible keeps the ring off mouse clicks, so nothing looks noisy. */
:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: 2px;
    border-radius: 2px;
}

.mud-button-root:focus-visible,
.mud-nav-link:focus-visible,
.mud-input-slot:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: 2px;
}

/* Respect the operating system's reduced-motion setting. Vestibular disorders are
   common enough that this is a correctness issue, not a preference. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* --------------------------------------------------------------------------
   Overflow containment
   Filter toolbars across the app set min-width on selects and search fields
   (120–240px) so they stay usable on a desktop. On a 375px phone two of them no
   longer fit side by side and push the page sideways. Rather than unpick every
   inline style, the constraint is released once the viewport is narrower than
   those fields were designed for.
   -------------------------------------------------------------------------- */
@media (max-width: 600px) {
    .mud-input-control[style*="min-width"],
    .mud-select[style*="min-width"],
    [style*="min-width:220px"],
    [style*="min-width:240px"],
    [style*="min-width: 220px"],
    [style*="min-width: 240px"] {
        min-width: 100% !important;
    }

    /* Dialogs are Small + FullWidth everywhere, which still leaves a hard min-width
       on some inner content. Let dialog bodies use the width they actually have. */
    .mud-dialog {
        margin: 12px;
        width: calc(100% - 24px);
    }
}

/* A table that cannot shrink scrolls inside its own box; the page body never does.
   Without this a single long id in one cell drags the whole layout sideways. */
.mud-table-container,
.mud-data-grid .mud-table-container {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

/* Opaque identifiers wrap instead of forcing a wider row. */
.mud-table-cell code,
.mud-table-cell .mud-chip-label {
    overflow-wrap: anywhere;
}

.mud-main-content {
    height: 100%;
    min-height: 100vh;
    display: flex;
}

.mud-chip.mud-chip-size-medium {
    font-size: var(--mud-typography-default-size) !important;
}

.mud-chip.mud-chip-size-small {
    font-size: var(--mud-typography-body2-size) !important;
}

.mud-chip {
    font-size: var(--mud-typography-default-size);
}
.mud-tabs {
    background-color: var(--mud-palette-surface);
}

.mud-input-control-margin-dense .mud-input > input.mud-input-root,
.mud-input-control-margin-dense div.mud-input-slot.mud-input-root {
    font-size: 0.90em;
}

.mud-input-control > .mud-input-control-input-container > .mud-input-label-inputcontrol {
    font-size: var(--mud-typography-default-size) !important;
}


.mud-simple-table table * tr > td, .mud-simple-table table * tr th {
    font-size: var(--mud-typography-default-size) !important;
}

.mud-expand-panel .mud-expand-panel-header {
    font-size: var(--mud-typography-default-size) !important;

}

.mud-button-year {
    font-size: var(--mud-typography-default-size) !important;
}

.mud-table-cell {
    font-size: var(--mud-typography-default-size) !important;
}
.mud-table-dense .mud-table-cell {
    font-size: var(--mud-typography-body2-size) !important;
}


.mud-button-outlined-size-small {
    font-size: var(--mud-typography-body2-size);
}

.mud-grid.readonly-grid > .mud-grid-item {
    border-bottom: 1px solid var(--mud-palette-table-lines);
    padding-bottom:2px;
}

.mud-nav-link {
    white-space: normal !important;
}

.user-button {
    text-transform: none;
    background: rgba(var(--mud-palette-primary-rgb), 0.1)
}

.side-menu .mud-chip.mud-chip-size-small {
    font-size: 0.625rem;
    height: 1.125rem;
}

.mud-table.mud-data-grid .mud-toolbar {
    height: auto !important;
    padding: 1rem
}


.mud-table.mud-data-grid .mud-toolbar-gutters {
    height: auto !important;
    padding: 1rem
}

.mud-table.mud-data-grid .mud-table-toolbar {
    height: auto !important;
    padding: 1rem
}

.mud-table.mud-data-grid .mud-table-pagination-toolbar {
    height: auto !important;
    padding: 0rem !important;
}



.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

.blazor-error-boundary::after {
    content: "An error has occurred."
}
/* ==========================================================================
   Authentication shell
   Two panes on a laptop, one on a phone. The brand pane is decoration and
   collapses first; the form is the only thing that must survive every width.
   ========================================================================== */

.auth-shell {
    min-height: 100dvh;          /* dvh, not vh: iOS Safari's address bar makes
                                    100vh taller than the visible viewport, which
                                    pushed the sign-in button off screen. */
    display: grid;
    grid-template-columns: 1fr;
    background: var(--mud-palette-background);
}

.auth-shell-plain {
    place-items: center;
    padding: 2rem;
}

@media (min-width: 900px) {
    .auth-shell {
        /* The brand pane takes the larger share but never grows unbounded on an
           ultrawide display, where a 60% pane would leave the form marooned. */
        grid-template-columns: minmax(0, 1.15fr) minmax(420px, 0.85fr);
    }
}

/* ---- brand pane ---- */

.auth-aside {
    display: none;
    position: relative;
    overflow: hidden;
    background: #0B0D12;
    padding: 3.5rem;
    align-content: end;
}

@media (min-width: 900px) {
    .auth-aside { display: grid; }
}

.auth-aside-art {
    position: absolute;
    inset: 0;
    /* Fades the graphic out where the copy sits, so the text never competes with a
       line crossing behind it. */
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 45%, transparent 88%);
    mask-image: linear-gradient(to bottom, #000 0%, #000 45%, transparent 88%);
}

.auth-aside-art svg {
    width: 100%;
    height: 100%;
    display: block;
}

/* ---- brand pane: the signal crossing the graph ----
   One wave every 8 seconds. Each element animates for a short slice of that and
   is invisible for the rest, so the pane is still most of the time — it sits
   behind a password field and must not pull the eye. --d, set per element in the
   markup, phases each tier after the one feeding it. */

.krino-flow path {
    /* The gap is longer than the path so the dash never wraps and appear twice.
       pathLength="100" in the markup is what makes one duration fit every
       segment regardless of its real length. */
    stroke-dasharray: 9 200;
    stroke-dashoffset: 15;          /* parked just before the start: invisible */
    animation: krino-signal 8s linear infinite;
    animation-delay: var(--d, 0s);
    will-change: stroke-dashoffset;
}

@keyframes krino-signal {
    0%   { stroke-dashoffset: 15; opacity: 0; }
    2%   { opacity: 0.95; }
    /* Travel occupies the first fifth of the cycle; the rest is rest. */
    18%  { opacity: 0.95; }
    20%  { stroke-dashoffset: -115; opacity: 0; }
    100% { stroke-dashoffset: -115; opacity: 0; }
}

.krino-pings circle {
    transform-box: fill-box;        /* scale about the circle's own centre rather
                                       than the SVG origin, which would fling the
                                       ring across the pane */
    transform-origin: center;
    opacity: 0;
    animation: krino-ping 8s ease-out infinite;
    animation-delay: var(--d, 0s);
}

@keyframes krino-ping {
    0%   { transform: scale(1);   opacity: 0; }
    3%   { opacity: 0.5; }
    14%  { transform: scale(3.4); opacity: 0; }
    100% { transform: scale(3.4); opacity: 0; }
}

.krino-nodes circle {
    animation: krino-node 8s ease-out infinite;
    animation-delay: var(--d, 0s);
}

/* The node brightens as the signal lands on it and settles back. Stroke width is
   left alone: growing it shifts the node's silhouette and reads as a wobble. */
@keyframes krino-node {
    0%   { stroke: #8B8BF0; }
    3%   { stroke: #C9C9FF; }
    16%  { stroke: #8B8BF0; }
    100% { stroke: #8B8BF0; }
}

/* Motion here is entirely decorative, so it is the first thing to go. What
   remains is the static graph the pane had before. */
@media (prefers-reduced-motion: reduce) {
    .krino-flow,
    .krino-pings {
        display: none;
    }

    .krino-nodes circle {
        animation: none;
    }
}

.auth-aside-body {
    position: relative;
    max-width: 34rem;
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}

.auth-aside-brand {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

.auth-aside-mark {
    width: 44px;
    height: auto;
    /* Lift the indigo off the ink ground without shifting its hue. */
    filter: brightness(1.45);
}

.auth-aside-wordmark {
    color: #F2F3F7;
    font-size: 1.4rem;
    font-weight: 600;
    letter-spacing: 0.06em;
}

.auth-aside-line {
    margin: 0;
    color: #F2F3F7;
    font-size: clamp(1.5rem, 2.1vw, 2rem);
    line-height: 1.25;
    font-weight: 600;
    letter-spacing: -0.02em;
    text-wrap: balance;
}

.auth-aside-sub {
    margin: 0;
    color: #9AA0B0;
    font-size: 0.95rem;
    line-height: 1.6;
    max-width: 30rem;
}

/* ---- form pane ---- */

.auth-main {
    display: grid;
    place-items: center;
    padding: 2rem 1.5rem;
    background: var(--mud-palette-surface);
}

@media (min-width: 900px) {
    .auth-main { padding: 3rem; }
}

.auth-card {
    width: 100%;
    max-width: 25rem;           /* ~400px: a login form wider than this reads as a
                                   settings page, not a single decision. */
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
}

.auth-card-logo {
    width: 56px;
    height: 56px;
    border-radius: 14px;
}

/* Once the brand pane is showing, the mark is already on screen twice; drop the
   smaller one rather than repeat it. */
@media (min-width: 900px) {
    .auth-card-logo { display: none; }
}

/* The identity forms are static-rendered, so they use raw inputs styled to match
   MudBlazor rather than MudTextField. Give them the breathing room the rest of
   the app has. */
.auth-card .mud-input-control { margin-block: 0 !important; }

.auth-card form > .mud-input-control + .mud-input-control { margin-top: 1rem !important; }

/* Visible field labels for the statically-rendered identity forms. These use raw
   inputs (MudTextField needs interactivity these pages do not have), so the label
   is styled here rather than inherited from MudBlazor. */
.auth-field-label {
    display: block;
    margin-bottom: 0.4rem;
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--mud-palette-text-secondary);
    order: -1;                       /* sits above its input despite the markup order */
}

.auth-card .mud-input-control-input-container {
    display: flex;
    flex-direction: column;
}

/* ==========================================================================
   Dashboard
   ========================================================================== */

/* Always a dark ground with light type, in either theme. Palette tokens flipped
   with the theme and turned this banner pale, taking the subtitle below readable
   contrast with it. */
.dashboard-hero {
    background: linear-gradient(135deg, #4646B8 0%, #0B0D12 100%);
    border-radius: var(--mud-default-borderradius);
}

.dashboard-hero-title { color: #FFFFFF !important; }
.dashboard-hero-sub   { color: rgba(255, 255, 255, 0.78) !important; }

/* An empty chart panel is mostly empty space. Centring the message and giving it
   an icon and a reason turns "nothing here" into "nothing yet, and here's why". */
.chart-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    min-height: 220px;
    text-align: center;
    color: var(--mud-palette-text-secondary);
}

.chart-empty .mud-icon-root { opacity: 0.35; }
.chart-empty .mud-typography-caption { opacity: 0.75; max-width: 28ch; }

/* Empty results inside a table. "No records" states what happened; the second line
   says what to do about it, which is the part that saves someone a support ticket. */
.table-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.4rem;
    padding: 3rem 1rem;
    text-align: center;
    color: var(--mud-palette-text-secondary);
}

.table-empty .mud-icon-root { opacity: 0.3; }
.table-empty .mud-typography-caption { opacity: 0.8; max-width: 34ch; }

/* Screen-level empty state (see Components/EmptyState.razor). Sits high rather than
   dead-centre in the viewport: a state pinned to the middle of a tall empty page
   reads as an error, while one just below the content edge reads as "not yet". */
.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    padding: 4rem 1.5rem;
    text-align: center;
}

.empty-state-icon {
    color: var(--mud-palette-text-secondary);
    opacity: 0.32;
}

.empty-state-body {
    color: var(--mud-palette-text-secondary);
    max-width: 44ch;
}

.empty-state-action { margin-top: 0.5rem; }

/* ---------------------------------------------------------------------------
   Responsive column priority for MudDataGrid

   MudTable can fold itself into cards below a breakpoint, but that only works
   because MudTd writes a data-label attribute the CSS reads back through
   ::before. MudDataGrid emits no such attribute, so the same trick renders a
   stack of unlabelled values. The six grid screens were therefore left with a
   ten-column table on a 375px phone: technically scrollable sideways, useless
   in practice.

   So the grids drop columns instead of folding them. Each column is tagged with
   how far down it survives, and what is left at phone width is what someone
   triaging on a phone actually needs: what the row is, how bad it is, and a way
   into the detail view. Everything hidden here is present on the detail screen.

   Both HeaderClass and CellClass have to carry the tag — hiding one without the
   other shifts every cell in the row one column left.
   --------------------------------------------------------------------------- */

@media (max-width: 959.98px) {
    .mud-table-cell.col-hide-md { display: none !important; }
}

@media (max-width: 599.98px) {
    .mud-table-cell.col-hide-sm { display: none !important; }
}

/* Long opaque values — ids, primary keys, stack traces — get to wrap at phone
   width rather than forcing the whole table wider than the screen. */
@media (max-width: 599.98px) {
    .mud-data-grid .mud-table-cell { word-break: break-word; }
}

/* Screen heading (see Components/PageHeader.razor). Title and its action share a
   band; below sm the action drops onto its own line and stretches, because a
   thumb-sized target beats a right-aligned one on a phone. */
.page-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
}

.page-header-text { min-width: 0; }

/* h5 lands at 18px, only two above the empty state's own heading — not enough of
   a step for the eye to read one as the page and the other as its content. */
.page-header-title {
    font-size: clamp(1.25rem, 1.1rem + 0.5vw, 1.5rem);
    line-height: 1.25;
    font-weight: 600;
    letter-spacing: -0.015em;
}

.page-header-description {
    color: var(--mud-palette-text-secondary);
    max-width: 68ch;
    margin-top: 0.125rem;
}

.page-header-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-shrink: 0;
}

/* Stacked, not shared: two buttons splitting a 390px row left "Create a new table"
   broken across two lines inside a button half the screen wide. One per line reads
   better and gives each a thumb-sized target. */
@media (max-width: 599.98px) {
    .page-header-actions {
        width: 100%;
        flex-direction: column;
        align-items: stretch;
    }
}

/* Submit feedback for enhanced forms (see wwwroot/js/form-busy.js). The button
   keeps its label — swapping the text for a spinner makes the button jump width
   and loses the one word confirming what is happening. */
.form-busy button[type="submit"],
.form-busy input[type="submit"] {
    position: relative;
    cursor: progress;
    opacity: 0.72;
}

.form-busy button[type="submit"]::after {
    content: "";
    position: absolute;
    inset-inline-end: 1rem;
    inset-block-start: calc(50% - 0.5rem);
    width: 1rem;
    height: 1rem;
    border: 2px solid currentColor;
    border-inline-end-color: transparent;
    border-radius: 50%;
    animation: form-busy-spin 0.6s linear infinite;
}

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

/* A spinner that cannot be stilled is worse than none for anyone who gets motion
   sick, so it holds at a fixed angle and the cursor carries the signal instead. */
@media (prefers-reduced-motion: reduce) {
    .form-busy button[type="submit"]::after { animation: none; }
}

/* ---------------------------------------------------------------------------
   Rule builder

   The rule is a tree, and indentation is how the tree is shown. A rail runs down
   the left of each group so the eye can follow which conditions belong together
   without counting pixels of indent.
   --------------------------------------------------------------------------- */

.rule-builder {
    container-type: inline-size;
}

.rule-group {
    border: 1px solid var(--mud-palette-divider);
    border-radius: var(--mud-default-borderradius);
    background: var(--mud-palette-surface);
}

/* The root sits inside the page's own card, so its border landed a hair from the
   rail and the two read as one thick double line — worst at phone width, where the
   indent is tightest. The surrounding card already frames it. */
.rule-group-root {
    border: none;
    border-radius: 0;
    background: transparent;
}

.rule-group-root > .rule-group-header {
    padding-inline: 0;
}

.rule-group-root > .rule-group-children {
    padding-inline-end: 0;
}

/* Nested groups sit on a tinted ground so depth is readable at a glance rather
   than inferred from how far right something starts. */
.rule-group:not(.rule-group-root) {
    background: var(--mud-palette-background-grey);
}

.rule-group-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.5rem;
    padding: 0.625rem 0.75rem;
    border-bottom: 1px solid var(--mud-palette-divider);
}

.rule-mode-select { max-width: 15rem; }

.rule-group-header-actions {
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

.rule-group-empty {
    margin: 0;
    padding: 1.25rem 0.75rem;
    text-align: center;
    color: var(--mud-palette-text-secondary);
    font-size: 0.875rem;
}

.rule-group-children {
    list-style: none;
    margin: 0;
    padding: 0.75rem 0.75rem 0.75rem 1.5rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    position: relative;
}

/* The rail. Sits inside the padding so rows keep their full width. */
.rule-group-children::before {
    content: "";
    position: absolute;
    inset-block: 0.75rem;
    inset-inline-start: 0.75rem;
    width: 1px;
    background: var(--mud-palette-divider);
}

.rule-group-child { position: relative; }

/* and / or between siblings, hung in the rail's margin rather than taking a row
   of its own — a full row per joiner doubles the height of every rule. */
.rule-joiner {
    position: absolute;
    /* Centred on the rail rather than sized to it: "and" is wider than the rail is,
       so a fixed width made the word hang off to one side of the line it labels. */
    inset-inline-start: -0.75rem;
    inset-block-start: -0.875rem;
    transform: translateX(-50%);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--mud-palette-text-secondary);
    background: var(--mud-palette-surface);
    padding: 0.0625rem 0.25rem;
    line-height: 1;
}

/* Right-to-left flips the inset but not the translate, which would push the word
   off the rail in the other direction. */
[dir="rtl"] .rule-joiner { transform: translateX(50%); }

.rule-group:not(.rule-group-root) .rule-joiner {
    background: var(--mud-palette-background-grey);
}

/* The root is transparent now, so the word needs the page's own ground to mask
   the rail behind it rather than the surface colour it used to sit on. */
.rule-group-root > .rule-group-children > .rule-group-child > .rule-joiner {
    background: var(--mud-palette-background-grey);
}

.rule-row {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.75rem;
    flex-wrap: wrap;
    padding: 0.625rem 0.75rem;
    border: 1px solid var(--mud-palette-divider);
    border-radius: var(--mud-default-borderradius);
    background: var(--mud-palette-surface);
}

.rule-row-fields,
.rule-row-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.rule-row-fields { flex: 1 1 22rem; min-width: 0; }

.rule-field-select { flex: 1 1 10rem; min-width: 8rem; }
.rule-operator-select { flex: 1 1 9rem; min-width: 8rem; }
.rule-value-slot { flex: 2 1 12rem; min-width: 8rem; }
.rule-aggregate-select { width: 8.5rem; }

/* Below the point where three controls stop fitting side by side, each takes the
   full width. Three selects sharing a phone-width row leaves none of them wide
   enough to show the value it holds. */
@container (max-width: 44rem) {
    .rule-row-fields,
    .rule-row-actions { width: 100%; }

    .rule-field-select,
    .rule-operator-select,
    .rule-value-slot,
    .rule-aggregate-select {
        flex: 1 1 100%;
        width: auto;
        min-width: 0;
    }
}

/* Indentation is the structure, so it cannot be given up entirely on a phone —
   but it can be tightened to buy back room for the controls. */
@container (max-width: 30rem) {
    .rule-group-children { padding-inline-start: 0.875rem; }
    .rule-group-children::before { inset-inline-start: 0.375rem; }
    .rule-joiner { inset-inline-start: -0.875rem; width: 0.875rem; }
}

/* ---------------------------------------------------------------------------
   Score histogram (see Pages/Analytics/ScoreHistogram.razor)
   --------------------------------------------------------------------------- */

.score-histogram { width: 100%; }

.score-histogram-plot {
    display: flex;
    align-items: flex-end;
    gap: 1px;
    height: 180px;
    padding: 0.25rem 0;
    border-bottom: 1px solid var(--mud-palette-divider);
}

.score-bar {
    flex: 1 1 0;
    min-width: 2px;
    border-radius: 1px 1px 0 0;
    /* Only the colour animates. Animating height as well made every threshold nudge
       look like the data had changed, when only the banding had. */
    transition: background-color 120ms ease;
}

@media (prefers-reduced-motion: reduce) {
    .score-bar { transition: none; }
}

.score-histogram-axis {
    display: flex;
    justify-content: space-between;
    font-size: 0.6875rem;
    font-variant-numeric: tabular-nums;
    color: var(--mud-palette-text-secondary);
    padding-top: 0.25rem;
}

.score-histogram-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 1rem;
    margin-top: 0.75rem;
    font-size: 0.8125rem;
}

.score-legend-item {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    font-variant-numeric: tabular-nums;
}

.score-legend-swatch {
    width: 0.625rem;
    height: 0.625rem;
    border-radius: 2px;
    flex-shrink: 0;
}

/* ---------------------------------------------------------------------------
   Link graph (see Pages/LinkAnalysis/Index.razor)
   --------------------------------------------------------------------------- */

.link-graph-canvas {
    width: 100%;
    height: 520px;
    border: 1px solid var(--mud-palette-divider);
    border-radius: var(--mud-default-borderradius);
    background: var(--mud-palette-background-grey);
    overflow: hidden;
}

.link-graph-svg { width: 100%; height: 100%; display: block; }

.link-graph-edge {
    stroke: var(--mud-palette-divider);
    stroke-width: 1.25;
}

.link-graph-node { cursor: pointer; }
.link-graph-node:focus-visible { outline: none; }
.link-graph-node:focus-visible circle,
.link-graph-node:focus-visible polygon {
    stroke: var(--mud-palette-primary);
    stroke-width: 3;
}

.link-graph-entity {
    fill: var(--mud-palette-surface);
    stroke: var(--mud-palette-text-secondary);
    stroke-width: 1.5;
}

/* The record the analyst opened is filled, not just larger — on a dense graph a size
   difference alone is easy to lose. */
.link-graph-entity.is-root {
    fill: #5B5BD6;
    stroke: #4646B8;
    stroke-width: 2;
}

.link-graph-identifier {
    fill: #F8BA03;
    stroke: var(--mud-palette-surface);
    stroke-width: 1.5;
}

/* Identifier kinds are separable by hue as well as by the diamond shape, because the
   strength of a link depends on what kind it is — a shared device is not a shared IP. */
.link-graph-identifier.kind-Device { fill: #D2371E; }
.link-graph-identifier.kind-Card { fill: #FF6600; }
.link-graph-identifier.kind-Email { fill: #14AA5F; }
.link-graph-identifier.kind-Phone { fill: #0E9AA7; }
.link-graph-identifier.kind-IpAddress { fill: #8B8BF0; }
.link-graph-identifier.kind-BankAccount { fill: #B45FBF; }

.link-graph-label {
    font-size: 10px;
    text-anchor: middle;
    fill: var(--mud-palette-text-secondary);
    pointer-events: none;
    font-variant-numeric: tabular-nums;
}

.link-graph-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 1.25rem;
    margin-top: 0.75rem;
    font-size: 0.8125rem;
    color: var(--mud-palette-text-secondary);
}

.link-legend-circle,
.link-legend-diamond {
    width: 0.7rem;
    height: 0.7rem;
    flex-shrink: 0;
    display: inline-block;
    border: 1.5px solid var(--mud-palette-text-secondary);
    background: var(--mud-palette-surface);
    border-radius: 50%;
}

.link-legend-circle.is-root { background: #5B5BD6; border-color: #4646B8; }

.link-legend-diamond {
    border-radius: 2px;
    transform: rotate(45deg);
    background: #F8BA03;
    border-color: var(--mud-palette-surface);
}

@media (max-width: 599.98px) {
    .link-graph-canvas { height: 360px; }
}
