/* ---------------------------------------------------------------------------
   Component styling, expressed in the design tokens from design-tokens.css.

   MudBlazor still provides the reset, layout and component behaviour; this file
   only adapts its surfaces to the idiom of www.oliver-kroener.de:

     * uppercase, widely-tracked buttons that lift on hover and press on click
     * generously rounded panels with a hairline border and a soft drop shadow
     * a warm orange accent bar marking the active item
     * a 3px focus ring at 2px offset (WCAG 2.4.13 Focus Appearance, AAA), the
       same rule the website applies

   Colours come from the tokens, so everything here follows the theme toggle.
   design-tokens.css is linked ahead of this file in App.razor rather than
   @import-ed, so it is fingerprinted and cached like every other asset.
   --------------------------------------------------------------------------- */

/* -- Foundations --------------------------------------------------------- */

html {
    scroll-behavior: smooth;
}

body {
    font-family: var(--font-family-base);
    background: var(--color-background);
    color: var(--color-foreground);
}

/* WCAG 2.4.13 Focus Appearance (Enhanced) — 3px, as on the website. Applied
   after MudBlazor's own focus styles so it wins on every control. */
:focus-visible,
.mud-button-root:focus-visible,
.mud-nav-link:focus-visible,
.mud-icon-button:focus-visible {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
}

/* FocusOnNavigate in Routes.razor moves focus to the page's h1 after every
   navigation so screen readers announce it, giving the heading tabindex="-1".
   That is not a control the reader can act on, and ringing it on arrival reads
   as a rendering fault, so the indicator is dropped for that case alone —
   every genuinely focusable control keeps the ring above. */
h1[tabindex="-1"]:focus,
h1[tabindex="-1"]:focus-visible {
    outline: none;
}

::selection {
    background: var(--color-primary-40);
    color: var(--color-foreground);
}

code,
kbd,
pre {
    font-family: var(--font-family-mono);
}

/* -- App bar ------------------------------------------------------------- */

/* Black and translucent with a hairline rule beneath, matching the site header.
   The bar is black in both themes, so its contents are always the light set. */
.mud-appbar.mud-appbar-fixed-top,
.mud-appbar {
    background-color: var(--color-header-bg);
    backdrop-filter: blur(var(--blur-md));
    -webkit-backdrop-filter: blur(var(--blur-md));
    border-bottom: var(--border-width-hairline) solid rgb(255 255 255 / 0.1);
    box-shadow: none;
}

/* Not a filled button: this rule would empty its fill to a 15% tint on hover and turn
   the public header's sign-in into a ghost of itself under the pointer. Filled buttons
   keep the lift and shadow from the Buttons block below instead. */
.mud-appbar .mud-icon-button:hover,
.mud-appbar .mud-button-root:not(.mud-button-filled):hover {
    background-color: var(--color-primary-15);
    color: var(--color-emphasis);
}

/* The wordmark: the site sets the surname in the brand orange. */
.app-brand {
    font-weight: var(--font-weight-bold);
    letter-spacing: var(--tracking-tight);
    text-transform: uppercase;
}

/* The lead word takes the app bar's own contrast colour, not MudLink's default
   primary. Two reasons, and they agree. The bar is black in both themes, and
   MudBlazor's primary reads 3.86:1 on it — the wordmark is 18px bold, just under
   the 18.66px that would let it pass at 3:1, so it needs 4.5:1 and does not
   reach it. And the lockup this is meant to be is white with the last word in
   brand orange; painting both words orange made the "two tones" the header
   describes a pair of oranges one step apart.

   !important because .mud-primary-text sets the colour !important. */
.app-brand.mud-primary-text {
    color: #fff !important;
}

.app-brand__accent {
    color: var(--color-emphasis);
}

/* -- Drawer and navigation ----------------------------------------------- */

.mud-drawer {
    border-right: var(--border-width-hairline) solid var(--color-border);
}

/* A closed temporary drawer (the public header's phone menu) is only moved off
   screen by MudBlazor, not hidden, so its links stayed in the tab order: the Tab
   after the header's sign-in went to an invisible "Solutions" at left -260px and
   seven more stops after it. visibility takes it out of the tab order and the
   accessibility tree. Delayed by MudBlazor's own 225ms slide so closing still
   animates; opening has no delay, because the open state declares no visibility
   transition and the drawer becomes visible before it moves. The transition
   list repeats MudBlazor's `left` because a declaration replaces it whole. */
.mud-drawer.mud-drawer-temporary.mud-drawer--closed {
    visibility: hidden;
}

.mud-drawer.mud-drawer-temporary.mud-drawer-pos-left.mud-drawer--closed:not(.mud-drawer--initial) {
    transition:
        left 225ms cubic-bezier(0, 0, 0.2, 1),
        visibility 0s linear 225ms;
}

.mud-nav-link {
    font-weight: var(--font-weight-semibold);
    letter-spacing: 0.01em;
    border-radius: 0 var(--radius-control) var(--radius-control) 0;
    min-height: var(--touch-target-min);
    transition:
        background-color var(--duration-fast) var(--ease-out),
        color var(--duration-fast) var(--ease-out);
}

.mud-nav-link:hover {
    background-color: var(--color-primary-08);
}

/* The active marker is the website's orange accent bar rather than a fill. */
.mud-nav-link.active {
    position: relative;
    background-color: var(--color-primary-10);
    color: var(--color-primary-readable);
}

.mud-nav-link.active::before {
    content: "";
    position: absolute;
    inset-block: 0;
    inset-inline-start: 0;
    width: 3px;
    border-radius: var(--radius-accent-bar);
    background: var(--color-primary);
}

.mud-nav-link.active .mud-icon-root {
    color: var(--color-primary-readable);
}

/* -- Buttons ------------------------------------------------------------- */

.mud-button-root {
    border-radius: var(--radius-control);
    text-transform: uppercase;
    letter-spacing: var(--tracking-button);
    font-weight: var(--font-weight-semibold);
    transition:
        background-color var(--duration-base) var(--ease-out),
        border-color var(--duration-base) var(--ease-out),
        color var(--duration-base) var(--ease-out),
        box-shadow var(--duration-base) var(--ease-out),
        transform var(--duration-base) var(--ease-out);
}

/* Filled and outlined buttons lift on hover and press in on click — the
   --btn-lift-hover / --btn-press-scale pair from the site's token set. */
.mud-button-filled:hover,
.mud-button-outlined:hover {
    transform: translateY(var(--btn-lift-hover));
}

.mud-button-filled:active,
.mud-button-outlined:active {
    transform: scale(var(--btn-press-scale));
}

.mud-button-filled-primary:hover {
    box-shadow: var(--shadow-button-hover);
}

.mud-button-filled-primary:active {
    box-shadow: var(--shadow-button-active);
}

.mud-button-outlined {
    border-width: var(--btn-border-width);
}

/* Text buttons keep the tracking but not the uppercasing inside menus, where
   they stand in for menu items and should read as labels, not as actions.
   Menu content renders into the popover provider at the root of the document
   rather than inside .mud-menu, so the popover is what these have to hang off. */
.mud-popover .mud-button-root,
.mud-menu-list .mud-button-root,
.mud-list-item .mud-button-root {
    text-transform: none;
    letter-spacing: normal;
    font-weight: var(--font-weight-medium);
}

.mud-icon-button {
    border-radius: var(--radius-control-sm);
}

.mud-fab {
    border-radius: var(--radius-control);
}

/* -- Panels, cards and dialogs ------------------------------------------- */

.mud-paper {
    border-radius: var(--radius-card);
}

.mud-paper-outlined {
    border: var(--border-width-hairline) solid var(--color-border);
}

.mud-card {
    border-radius: var(--radius-card);
    border: var(--border-width-hairline) solid var(--color-border);
    box-shadow: var(--shadow-card);
    transition:
        border-color var(--duration-slow) var(--ease-out),
        box-shadow var(--duration-slow) var(--ease-out),
        transform var(--duration-slow) var(--ease-out);
}

.mud-card:hover {
    border-color: var(--color-primary-40);
    box-shadow: var(--shadow-card-hover);
    transform: translateY(var(--btn-lift-hover));
}

.mud-dialog {
    border-radius: var(--radius-surface);
    border: var(--border-width-hairline) solid var(--color-border);
    box-shadow: var(--shadow-panel);
}

.mud-dialog-title {
    font-weight: var(--font-weight-bold);
    letter-spacing: var(--tracking-tight);
}

.mud-popover,
.mud-menu-list {
    border-radius: var(--radius-xl);
    border: var(--border-width-hairline) solid var(--color-border);
    box-shadow: var(--shadow-panel);
}

.mud-tooltip-root .mud-tooltip {
    border-radius: var(--radius-control-sm);
}

/* -- Inputs -------------------------------------------------------------- */

.mud-input.mud-input-outlined .mud-input-outlined-border {
    border-radius: var(--radius-input);
}

.mud-input-control .mud-input:focus-within {
    caret-color: var(--color-primary);
}

/* MudSelectItem renders as a list item, so this covers select options too. */
.mud-list-item {
    border-radius: var(--radius-control-sm);
}

/* -- Chips, alerts and badges -------------------------------------------- */

.mud-chip {
    border-radius: var(--radius-pill);
    font-weight: var(--font-weight-semibold);
    letter-spacing: var(--tracking-wide);
}

.mud-alert {
    border-radius: var(--radius-xl);
}

/* A filled alert paints a brand-strength colour behind everything it contains,
   and MudBlazor re-points only its *buttons* at the alert's contrast colour.
   Everything else keeps the value it would have had on an ordinary page
   surface, which is how the impersonation banner ended up failing WCAG 1.4.3
   and 1.4.11 on three counts at once: .mud-chip-outlined stays
   --mud-palette-text-primary (near-black on the filled warning — 3.92:1, under
   the 4.5:1 that normal text needs), its border stays a 24%-black hairline
   (1.51:1), and the focus ring stays brand orange (1.47:1) — invisible on the
   one control that ends an impersonation.

   So inside a filled alert everything defers to the alert's own contrast
   colour, which AppTheme.cs guarantees against that fill in both modes: white
   on the light statuses, black on the dark ones. The attribute selector is
   deliberate — this has to hold for all nine severities, not the one severity
   this app happens to use today. */
.mud-alert[class*="-filled-"] {
    --color-focus: currentColor;
}

.mud-alert[class*="-filled-"] .mud-chip-outlined,
.mud-alert[class*="-filled-"] .mud-button-outlined {
    color: currentColor;
    border-color: currentColor;
}

/* -- Tables and data grid ------------------------------------------------ */

.mud-table {
    border-radius: var(--radius-card);
}

.mud-table-head .mud-table-cell {
    font-weight: var(--font-weight-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    font-size: var(--font-size-xs);
    color: var(--color-muted);
}

.mud-table-row:hover .mud-table-cell {
    background-color: var(--color-primary-05);
}

/* -- Tabs ---------------------------------------------------------------- */

.mud-tabs-tabbar {
    border-radius: var(--radius-card) var(--radius-card) 0 0;
}

.mud-tab {
    text-transform: uppercase;
    letter-spacing: var(--tracking-button);
    font-weight: var(--font-weight-semibold);
    min-height: var(--touch-target-min);
}

.mud-tab-slider {
    border-radius: var(--radius-accent-bar);
    background: var(--color-primary);
}

/* -- Page furniture ------------------------------------------------------ */

/* The website's section eyebrow: small, uppercase, widely tracked, in the
   brand orange, sitting above a heading. */
.app-eyebrow {
    display: block;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-bold);
    letter-spacing: var(--tracking-widest);
    text-transform: uppercase;
    color: var(--color-primary-readable);
}

/* And its accent bar, used to the left of a page or hero heading. */
.app-accent-bar {
    padding-inline-start: 1.25rem;
    border-inline-start: var(--border-width-emphasis) solid var(--color-primary);
}

/* The website's .text-balance: at hero weight and size, an orphaned last word
   is very visible, so let the browser even the lines out. */
.app-accent-bar .mud-typography-h1 {
    text-wrap: balance;
    /* The last resort only: a word that still does not fit breaks rather than
       widening the page. */
    overflow-wrap: break-word;
}

/* At 48px, "applications" alone is wider than a 360px phone. It widened the
   whole page and pushed the header's sign-in past the screen edge. Smaller type
   rather than hyphens: hyphenating a heavy headline split nearly every line, and
   at 36px "Anwendungen," still broke before its comma. */
@media (max-width: 599.98px) {
    .app-accent-bar .mud-typography-h1 {
        font-size: 2rem;
    }
}

/* -- Blazor error banner ------------------------------------------------- */

/* Rendered outside MudLayout, so it gets its styling here. Warning amber on a
   dark panel keeps it legible in either theme without borrowing the brand
   orange, which would read as an ordinary accent rather than a problem. */
#blazor-error-ui {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 2000;
    display: none;
    gap: 1rem;
    align-items: center;
    padding: 0.875rem 1.5rem;
    background: #1a1a1a;
    color: #ededed;
    border-top: var(--border-width-emphasis) solid #f0b400;
    box-shadow: 0 -12px 40px -14px rgb(0 0 0 / 0.7);
    font-family: var(--font-family-base);
    font-size: var(--font-size-sm);
}

#blazor-error-ui .reload {
    color: #f0b400;
    font-weight: var(--font-weight-semibold);
    text-underline-offset: 3px;
}

#blazor-error-ui .dismiss {
    position: absolute;
    top: 0.5rem;
    right: 1rem;
    cursor: pointer;
    line-height: 1;
}

/* -- Reduced motion ------------------------------------------------------ */

/* WCAG 2.3.3 Animation from Interactions (AAA), matching the website. The
   transforms above are the reason this matters: a lifting, scaling button is
   exactly the kind of motion this setting asks us to drop. */
@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;
    }

    .mud-button-filled:hover,
    .mud-button-outlined:hover,
    .mud-button-filled:active,
    .mud-button-outlined:active,
    .mud-card:hover {
        transform: none;
    }
}

/* --------------------------------------------------------------------------
   ROI calculator: the result panel tracks the form on a wide screen.

   Sticky rather than fixed, so it stops at the end of its grid column instead
   of floating over the history table below it. The offset clears the fixed app
   bar (64px) plus the container's top margin. Below the md breakpoint the two
   columns stack and stickiness would pin the result over the fields it
   describes, so it is switched off there.
   -------------------------------------------------------------------------- */
.app-roi-sticky {
    position: sticky;
    top: 5.5rem;
}

@media (max-width: 959.98px) {
    .app-roi-sticky {
        position: static;
    }
}

/* --------------------------------------------------------------------------
   Likert scale: one option per line.

   MudRadioGroup renders a `display:flex; flex-wrap:wrap` row and does not pass
   its Class parameter through to that element, so the direction cannot be set
   from the component. The KI-Reifegrad statements are full sentences; wrapped
   into a ragged two-column block the ascending order of the scale — which is
   what encodes the score — stops being readable.
   -------------------------------------------------------------------------- */
.app-scale .mud-input-control.mud-input-control-boolean-input .mud-radio-group {
    /* Matching MudBlazor's own selector, plus .app-scale, so this wins on specificity —
       `.mud-input-control.mud-input-control-boolean-input .mud-radio-group` sets
       `flex-flow: wrap` and outranks a plainer rule. Overriding the same shorthand it
       uses rather than flex-direction, so nothing is left half-set. */
    flex-flow: column nowrap;
    align-items: flex-start;
}

/* --------------------------------------------------------------------------
   Lizenzmanager: the spend breakdown bars.

   One measure, one hue. Ranking annual spend across categories is a single
   series, so there is no palette to assign and nothing for colour to encode
   beyond "this is the bar" — every row wears --color-primary and identity comes
   from the label beside it, never from the fill. That also means the list stays
   readable in greyscale, under forced colours, and to a reader who cannot
   separate hues.

   The bars are a presentational echo of numbers that are already written out
   next to them, so they are aria-hidden and the row carries the text.
   -------------------------------------------------------------------------- */
.app-spend-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: baseline;
    gap: 0.5rem 1rem;
}

.app-spend-track {
    grid-column: 1 / -1;
    /* Thin: the bar ranks, the number quantifies. A tall bar would compete with
       the label for the row's attention and win. */
    height: 0.5rem;
    border-radius: var(--radius-sm);
    background-color: var(--mud-palette-action-disabled-background);
    overflow: hidden;
}

.app-spend-fill {
    height: 100%;
    /* Rounded only at the data end, anchored to the baseline at the other, so
       the bar reads as growing from zero rather than floating. */
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    background-color: var(--color-primary);
    /* A zero-width bar still needs to be visible as "present but negligible". */
    min-width: 2px;
}

/* --------------------------------------------------------------------------
   Bildzuschnitt: the crop dialog's stage and its controls.

   Not scoped CSS, because the checkerboard and the frame are what tell the user
   that the padding around a fitted logo is transparent rather than white, and
   that is a statement about the stored file — the same statement anywhere the
   cropper is used. It belongs with the other app-wide rules here.
   -------------------------------------------------------------------------- */
.app-crop-stage {
    position: relative;
    width: 100%;
    /* The frame is the shape of the file it produces — square for a portrait, a
       wide band for a logo — because the export replays the on-screen transform
       at the output resolution, and a frame of a different shape would store a
       picture other than the one being positioned. The dialog sets the ratio;
       the fallback keeps the frame usable if it ever renders without one.
       Capped so the dialog still fits a laptop screen with the controls and the
       actions below it. */
    max-width: 22rem;
    aspect-ratio: var(--app-crop-aspect, 1);
    /* A size container so the empty state below can respond to the frame's own
       height. The frame is short for a logo and tall for a portrait at the same
       viewport, so a media query would answer the wrong question. Safe as a size
       container because the two properties above give it a definite size that its
       contents do not influence. */
    container: app-crop / size;
    margin-inline: auto;
    border-radius: var(--radius-md);
    overflow: hidden;
    /* The checkerboard shows through wherever the picture does not reach, which
       is the only way a transparent margin around a logo is distinguishable from
       a white one before it is saved.

       Light in both themes, and the one place in the app that ignores the toggle.
       Made of theme neutrals it goes almost black in dark mode, and a logo drawn
       in black — which is most of them, because most were drawn for paper —
       disappears into it exactly when the user is trying to position it. A pale
       ground is also the truer preview: the card these end up on is light. */
    background-color: #fff;
    background-image: repeating-conic-gradient(#e0e0e0 0% 25%, transparent 0% 50%);
    background-size: 1rem 1rem;
    outline: 1px solid var(--color-border);
}

.app-crop-stage.app-crop-over {
    outline: 2px dashed var(--color-primary);
}

.app-crop-canvas {
    display: block;
    width: 100%;
    height: 100%;
    /* The canvas is dragged, so the browser must not claim the gesture for
       scrolling or for a text selection first. */
    touch-action: none;
    cursor: grab;
}

.app-crop-canvas:active {
    cursor: grabbing;
}

.app-crop-canvas:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: -2px;
}

.app-crop-empty {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 1rem;
    /* On the light stage, not on the theme's surface — see .app-crop-stage. */
    color: rgba(0, 0, 0, 0.6);
    /* Over an empty canvas, so the drop target underneath still sees the drag —
       the listener is on the stage, and this panel is inside it. */
    background-color: #fff;
}

/* The logo's frame is a short band, and the stacked icon, caption and button do
   not fit one — they would be clipped by the frame's own overflow, which is the
   kind of fault that only shows up in the slot nobody tested. Below ten rem the
   same three parts lay out in a row instead, which fits every frame this dialog
   can produce. */
@container app-crop (max-height: 10rem) {
    .app-crop-empty {
        flex-direction: row;
        justify-content: center;
        gap: 0.5rem;
        padding: 0.5rem;
    }

    /* Margins that space a column read as gaps in a row. */
    .app-crop-empty .mud-typography {
        margin: 0;
    }

    .app-crop-empty .mud-icon-root {
        font-size: 1.5rem;
    }
}

.app-crop-controls {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    max-width: 22rem;
    margin: 0.75rem auto 0;
}

.app-crop-slider {
    flex: 1 1 auto;
    min-width: 0;
    accent-color: var(--color-primary);
}

.app-crop-slider:disabled {
    opacity: 0.4;
}

/* Visually hidden rather than display:none: the button handlers click this input
   to open the native picker, and a display:none control is not reliably
   clickable. Off the tab order and hidden from assistive technology, because the
   labelled buttons are what operate it. */
.app-crop-file {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

.app-crop-restart {
    display: flex;
    justify-content: flex-end;
    max-width: 22rem;
    margin: 0.25rem auto 0;
}

.app-crop-hint {
    display: block;
    max-width: 22rem;
    margin: 0.25rem auto 0;
}

/* -- MCP setup guide ----------------------------------------------------- */

/* A block of client configuration the user copies out. Monospaced, scrollable
   in its own right rather than widening the page — a claude_desktop_config.json
   line is long and a phone is 380 pixels wide — and never wrapped, because a
   wrapped JSON string is a JSON string somebody pastes with a newline in it. */
.app-mcp-code {
    position: relative;
    margin: 0;
    padding: 0.875rem 3rem 0.875rem 1rem;
    overflow-x: auto;
    background: var(--color-surface-light);
    border: var(--border-width-hairline) solid var(--border-color-subtle);
    border-radius: var(--radius-md);
    font-size: 0.8125rem;
    line-height: 1.55;
    white-space: pre;
    tab-size: 2;
}

/* The copy button sits over the block's top-right corner, which is what the
   right-hand padding above is reserving. Absolute rather than in the flow so a
   long single line still scrolls under it instead of pushing it away. */
.app-mcp-code-copy {
    position: absolute;
    top: 0.375rem;
    right: 0.375rem;
}

.app-mcp-code-wrap {
    position: relative;
}

/* One step of the guide. The number is a real list marker so a screen reader
   announces "3 of 5" rather than reading a decorative circle. */
.app-mcp-steps {
    margin: 0;
    padding-left: 1.5rem;
}

.app-mcp-steps > li {
    margin-bottom: 1rem;
}

.app-mcp-steps > li::marker {
    color: var(--color-primary-readable);
    font-weight: 600;
}

/* The permission matrix. Four switches per row want to stay on one line on a
   laptop and stack on a phone rather than overflow. */
.app-mcp-matrix {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
    gap: 0.5rem 1rem;
}

/* One collapsible client in the MCP set-up guide. Native <details>, styled to
   match MudPaper Outlined — see the comment in McpSetupGuide.razor for why it is
   not a MudExpansionPanel. */
.app-mcp-clients {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.app-mcp-client {
    background: var(--color-surface);
    border: var(--border-width-hairline) solid var(--border-color-subtle);
    border-radius: var(--radius-md);
}

.app-mcp-client > summary {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    /* 44px tall including the padding: --touch-target-min, which is what a
       control standing on its own should use (SC 2.5.8 asks only for 24). */
    min-height: var(--touch-target-min, 44px);
    padding: 0.5rem 1rem;
    border-radius: var(--radius-md);
    cursor: pointer;
    font-weight: 600;
    /* The default disclosure triangle is replaced by the chevron below, which
       sits at the end of the row where MudBlazor's panels put theirs. */
    list-style: none;
}

.app-mcp-client > summary::-webkit-details-marker {
    display: none;
}

/* The chevron. A rotated border-drawn caret rather than an icon font, so it
   inherits currentColor and needs no asset — and it is decorative: <details>
   already exposes the open state to assistive technology, so nothing here has
   to convey it in text. */
.app-mcp-client > summary::after {
    content: "";
    width: 0.5rem;
    height: 0.5rem;
    margin-left: auto;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg) translate(-0.125rem, -0.125rem);
    transition: transform 150ms ease;
    opacity: 0.7;
}

.app-mcp-client[open] > summary::after {
    transform: rotate(225deg) translate(-0.125rem, -0.125rem);
}

.app-mcp-client > summary:hover {
    background: var(--color-primary-05);
}

.app-mcp-client-body {
    padding: 0 1rem 1rem;
}

/* -- The public marketing site ------------------------------------------- */

/* PublicLayout puts @Body straight into <main> with no MudContainer, so every
   band paints edge to edge and re-imposes its own measure with .pub-wrap. That
   split — full-bleed background, contained text — is the whole reason the
   public site has a layout of its own. */

.pub-wrap {
    width: 100%;
    max-width: 72rem;
    margin-inline: auto;
    padding-inline: 1.5rem;
}

.pub-section {
    /* Anchored sections (the footer links to #barrierefreiheit) must clear the
       fixed app bar, or the heading lands underneath it. */
    scroll-margin-top: 5rem;
    padding-block: 4.5rem;
}

.pub-section--subtle {
    background: var(--color-surface-light);
}

/* Ink: near-black in both themes, and the ground every hero and closing band
   stands on.

   Both halves of each pair below are needed, and the reason is a genuine trap.
   design-tokens.css declares --color-foreground: var(--mud-palette-text-primary)
   at :root, and a var() inside a custom-property declaration is substituted at
   computed-value time *on the element the declaration applies to* — :root. So
   --color-foreground computes to the light theme's near-black once, at the root,
   and inherits that resolved colour down: overriding --mud-palette-text-primary
   here does not retroactively change it. Re-pointing only the palette entries
   leaves 1.02:1 black-on-black headings in light mode, which is how this was
   found.

   The palette entries are still set because MudBlazor's own components inside
   the band (MudLink, an outlined MudButton, MudIcon) read them directly rather
   than through the tokens. */
.pub-ink {
    --mud-palette-text-primary: #fff;
    --mud-palette-text-secondary: #c9ced4;
    --mud-palette-lines-default: rgb(255 255 255 / 0.18);

    --color-foreground: #fff;
    --color-muted: #c9ced4;
    --color-border: rgb(255 255 255 / 0.18);

    /* --color-primary-readable resolves through light-dark(), which keys off
       the *page's* color-scheme and cannot know it is inside an ink band. In
       light mode it would hand back the 0.56 step, which is ~3.6:1 on this
       ground and fails SC 1.4.3 as text. On ink the bright step is 6.10:1 and
       is the correct one in both themes. */
    --color-primary-readable: var(--color-primary);

    background: #0d0d0d;
    color: var(--color-foreground);
}

/* The focus ring is brand orange, which clears 3:1 on this ink — so it is kept
   rather than re-pointed. Stated explicitly because the ring is the one thing
   a coloured surface most often swallows. */
.pub-ink :focus-visible {
    outline-color: var(--color-focus);
}

/* MudBlazor paints link text from --mud-palette-primary, which AppTheme.cs sets
   to the darker orange on purpose: it is the step that carries white text on a
   filled button (5.07:1). Read *as text* on this ink it is only 3.84:1 and
   fails SC 1.4.3, so links alone move to the bright step, which is 6.10:1 here.

   The palette entry itself is deliberately left alone. Re-pointing it would
   also repaint every filled primary button standing on this ink, and white on
   the bright step is 3.44:1 — trading a failing link for a failing button. */
.pub-ink .mud-link.mud-primary-text,
.pub-ink .pub-card__link.mud-primary-text {
    /* !important, which this file otherwise avoids, because MudBlazor's own
       .mud-primary-text declares the colour !important — a plain declaration
       here loses however specific the selector is. The alternative, giving the
       links Color.Inherit, would paint them the same white as the prose around
       them and leave nothing but a hover underline to say they are links, which
       trades a contrast failure for an SC 1.4.1 one. */
    color: var(--color-primary-readable) !important;
}

.pub-hero {
    padding-block: 6rem 5rem;
}

/* The answer paragraph under a heading. Held to a narrower measure than the
   band: ~68 characters is the line length long-form prose stays readable at. */
.pub-lede {
    max-width: 46rem;
    margin-block: 1rem 0;
    font-size: var(--font-size-lg);
    line-height: var(--leading-relaxed);
    color: var(--color-muted);
}

.pub-hero__lede {
    font-size: var(--font-size-xl);
    max-width: 48rem;
}

/* A hero with an aside (the home page's sign-in panel): stacked below md, side by
   side from it. minmax(0, 1fr) in both, because a grid track otherwise refuses to
   shrink below its longest word: the headline's "applications" at h1 size then
   widened a 360px phone by 10px and pushed the header's sign-in off the edge. */
.pub-hero__grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 3rem;
    align-items: center;
}

@media (min-width: 960px) {
    .pub-hero__grid {
        grid-template-columns: minmax(0, 1fr) 24rem;
    }
}

/* The sign-in panel. The border is the bright orange, 6.10:1 on the ink and the
   thing that makes this read as a box you act in rather than one more paragraph
   (SC 1.4.11 asks 3:1). The fill is the same faint white the ink cards use. */
.pub-signin {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    padding: 2rem;
    border: 2px solid var(--color-primary);
    border-radius: var(--radius-card);
    background: rgb(255 255 255 / 0.04);
}

.pub-signin__icon {
    color: var(--color-primary);
}

.pub-signin__heading {
    margin: 0;
    font-size: var(--font-size-2xl);
    font-weight: var(--font-weight-extrabold);
    letter-spacing: var(--tracking-tight);
    line-height: var(--leading-tight);
}

.pub-signin__body,
.pub-signin__note {
    margin: 0;
    color: var(--color-muted);
    line-height: var(--leading-relaxed);
}

.pub-signin__body {
    margin-bottom: 0.5rem;
}

.pub-signin__note {
    font-size: var(--font-size-sm);
}

/* The header's sign-in sits after an icon button or the outlined call button; the
   gap keeps the two targets from reading as one control. */
.app-signin {
    margin-left: 0.5rem;
    white-space: nowrap;
    /* The bar gives up width from the wordmark, which wraps, never from this. */
    flex-shrink: 0;
}

/* On a phone the bar is menu, wordmark, language and this, and at 360px the
   German "Anmelden" with its icon is 18px wider than what is left. The icon goes
   and the word stays: the word is what somebody looking for the login reads, the
   glyph only repeats it. */
@media (max-width: 599.98px) {
    .mud-appbar .app-signin .mud-button-icon-start {
        display: none;
    }
}

.pub-section__heading {
    margin: 0;
    font-size: var(--font-size-3xl);
    font-weight: var(--font-weight-extrabold);
    letter-spacing: var(--tracking-tight);
    line-height: var(--leading-tight);
    text-wrap: balance;
}

.pub-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    margin-top: 2.5rem;
}

/* -- Cards, facts, steps, questions -------------------------------------- */

.pub-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
    gap: 1.5rem;
    margin-top: 2.5rem;
}

.pub-card {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding: 1.75rem;
    border: var(--border-width-hairline) solid var(--color-border);
    border-radius: var(--radius-card);
    background: var(--color-surface);
    box-shadow: var(--shadow-card);
}

/* On ink the surface token is still the page's, which would paint a light card
   on a black band. Transparent instead, so the card reads as an outline. */
.pub-ink .pub-card {
    background: rgb(255 255 255 / 0.04);
    box-shadow: none;
}

.pub-card__icon {
    margin-bottom: 0.25rem;
}

.pub-card__title {
    margin: 0;
    font-size: var(--font-size-xl);
    font-weight: var(--font-weight-bold);
    line-height: var(--leading-tight);
}

.pub-card__body {
    margin: 0;
    color: var(--color-muted);
    line-height: var(--leading-relaxed);
}

.pub-card__link {
    margin-top: auto;
    padding-top: 0.75rem;
    font-weight: var(--font-weight-semibold);
    color: var(--color-primary-readable);
}

/* "At a glance" — a definition list, so an assistant lifting the page keeps
   each value attached to its term instead of paraphrasing a paragraph. */
.pub-facts {
    margin-top: 2.5rem;
    border-top: var(--border-width-hairline) solid var(--color-border);
}

.pub-facts__row {
    display: grid;
    grid-template-columns: minmax(10rem, 18rem) 1fr;
    gap: 0.5rem 2rem;
    padding-block: 1rem;
    border-bottom: var(--border-width-hairline) solid var(--color-border);
}

@media (max-width: 40rem) {
    .pub-facts__row {
        grid-template-columns: 1fr;
        gap: 0.25rem;
    }
}

.pub-facts__term {
    font-weight: var(--font-weight-bold);
}

.pub-facts__value {
    margin: 0;
    color: var(--color-muted);
}

/* An ordered process. The number is the content, so it is rendered from the
   list itself rather than typed into each title. */
.pub-steps {
    counter-reset: pub-step;
    list-style: none;
    margin-top: 2.5rem;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
    gap: 2rem;
}

.pub-steps__item {
    counter-increment: pub-step;
    padding-top: 1rem;
    border-top: var(--border-width-emphasis) solid var(--color-primary);
}

/* Decorative: the <ol> already conveys the order to assistive technology, so
   this must not be read out a second time. */
.pub-steps__item::before {
    content: counter(pub-step);
    display: block;
    margin-bottom: 0.5rem;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-bold);
    letter-spacing: var(--tracking-widest);
    color: var(--color-primary-readable);
}

.pub-steps__title {
    margin: 0 0 0.5rem;
    font-size: var(--font-size-lg);
    font-weight: var(--font-weight-bold);
}

.pub-steps__body {
    margin: 0;
    color: var(--color-muted);
    line-height: var(--leading-relaxed);
}

.pub-faq {
    margin-top: 2.5rem;
    display: grid;
    gap: 2rem;
    max-width: 52rem;
}

.pub-faq__question {
    margin: 0 0 0.5rem;
    font-size: var(--font-size-xl);
    font-weight: var(--font-weight-bold);
    line-height: var(--leading-tight);
}

.pub-faq__answer {
    margin: 0;
    color: var(--color-muted);
    line-height: var(--leading-relaxed);
}

/* -- Prose, for the two legal pages -------------------------------------- */

.pub-prose {
    max-width: 46rem;
    margin-top: 1.5rem;
}

.pub-prose p {
    margin-block: 0 1rem;
    line-height: var(--leading-relaxed);
}

/* A data table inside legal prose (the app privacy policy's "what goes where").
   Words break and hyphenate rather than widening the page: German compounds
   like "Kopplungsgeheimnis" in three columns overflow 360px otherwise (SC 1.4.10). */
.pub-table {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: 1rem;
    /* Its own surface: the light page background is within a shade of
       --color-surface-light, so the header row would not read as one. */
    background: var(--color-surface);
    font-size: 0.95rem;
    line-height: var(--leading-relaxed);
    hyphens: auto;
    overflow-wrap: break-word;
}

.pub-table caption {
    text-align: left;
    margin-bottom: 0.5rem;
    color: var(--color-muted);
}

.pub-table th,
.pub-table td {
    border: var(--border-width-hairline) solid var(--color-border);
    padding: 0.4rem 0.5rem;
    text-align: left;
    vertical-align: top;
}

.pub-table th {
    font-weight: var(--font-weight-bold);
    background: var(--color-surface-light);
}

/* Three columns in 312px: "(Android/iOS)," cannot hyphenate and at the full size
   pushes the table past the gutter. Tighter here rather than overflow-wrap:
   anywhere, which fits by splitting "iOS" across two lines. */
@media (max-width: 30rem) {
    .pub-table {
        font-size: 0.875rem;
    }

    .pub-table th,
    .pub-table td {
        padding: 0.35rem 0.3rem;
    }
}

/* -- Header, breadcrumb, footer ------------------------------------------ */

/* Visible only once focused — the first stop on the page, and it has to be
   seen when it lands. */
.pub-skip {
    position: absolute;
    left: 1rem;
    top: -10rem;
    z-index: 2000;
    padding: 0.75rem 1.25rem;
    border-radius: var(--radius-control);
    background: var(--color-primary);
    color: var(--color-primary-foreground);
    font-weight: var(--font-weight-bold);
    transition: top var(--duration-fast) var(--ease-out);
}

.pub-skip:focus {
    top: 1rem;
}

.pub-nav {
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

.pub-nav__link {
    font-weight: var(--font-weight-semibold);
}

/* The theme toggle in the phone drawer reads as one more entry of the menu above
   it, not as a call to action: sentence case, left-aligned like the nav links. */
.pub-drawer__action {
    text-transform: none;
    letter-spacing: 0.01em;
    min-height: var(--touch-target-min);
}

.pub-drawer__action .mud-button-label {
    justify-content: flex-start;
    text-align: start;
}

.pub-breadcrumb {
    padding-block: 1.25rem;
}

.pub-breadcrumb__list {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: var(--font-size-sm);
}

.pub-breadcrumb__item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.pub-breadcrumb__sep {
    color: var(--color-muted);
}

.pub-breadcrumb [aria-current="page"] {
    color: var(--color-muted);
}

.pub-footer {
    padding-block: 4rem 2.5rem;
}

.pub-footer__columns {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    gap: 2.5rem;
}

.pub-footer__heading {
    margin: 0 0 1rem;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-bold);
    letter-spacing: var(--tracking-widest);
    text-transform: uppercase;
    color: var(--color-primary-readable);
}

.pub-footer__list {
    margin: 0;
    padding: 0;
    list-style: none;
    display: grid;
    gap: 0.5rem;
}

.pub-footer__address {
    font-style: normal;
    line-height: var(--leading-relaxed);
    color: var(--color-muted);
}

.pub-footer__legalname {
    font-weight: var(--font-weight-bold);
    color: var(--color-foreground);
}

.pub-footer__language {
    margin-top: 0.75rem;
}

.pub-footer__note {
    margin: 3rem 0 0;
    padding-top: 1.5rem;
    border-top: var(--border-width-hairline) solid var(--color-border);
    font-size: var(--font-size-sm);
    color: var(--color-muted);
}

.pub-cta__heading {
    margin: 0;
    font-size: var(--font-size-3xl);
    font-weight: var(--font-weight-extrabold);
    letter-spacing: var(--tracking-tight);
    line-height: var(--leading-tight);
    text-wrap: balance;
}

/* == Blog ================================================================= */

/* Three by two on a desktop, two by three on a tablet, one column on a phone.
   Explicit breakpoints rather than the auto-fit used by .pub-grid: six cards
   under auto-fit becomes four-and-two on a wide screen, which is a ragged last
   row, and the whole point of the page size is that it fills the viewport once.
   See PublicBlogService.PageSize. */
.blog-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1.5rem;
    margin-top: 2.5rem;
}

@media (min-width: 40rem) {
    .blog-grid {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (min-width: 64rem) {
    .blog-grid {
        grid-template-columns: repeat(3, 1fr);
    }
}

.blog-card {
    display: flex;
    /* Establishes the containing block that .blog-card__link::after resolves
       against, which is what lets the headline's anchor cover the whole card. */
    position: relative;
    flex-direction: column;
    overflow: hidden;
    border: var(--border-width-hairline) solid var(--color-border);
    border-radius: var(--radius-card);
    background: var(--color-surface);
    box-shadow: var(--shadow-card);
    transition: box-shadow var(--duration-fast) var(--ease-out);
}

.blog-card:hover {
    box-shadow: var(--shadow-card-hover);
}

/* The ring is drawn on the card rather than on the link inside it, because the
   whole card is the click target and a ring around the headline alone would
   point at a fraction of what is actually focused. :focus-within is what moves
   it there when the real, keyboard-reachable link takes focus. */
.blog-card:focus-within {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
}

.blog-card__media {
    display: block;
    width: 100%;
    /* Reserving the box before the picture arrives — without it the whole grid
       reflows as each of the six images loads, which is both a layout shift
       and, on a slow connection, a page that moves under the reader's cursor. */
    aspect-ratio: 1200 / 630;
    object-fit: cover;
    background: var(--color-surface-light);
}

.blog-card__body {
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: 0.5rem;
    padding: 1.5rem;
}

.blog-card__title {
    margin: 0;
    font-size: var(--font-size-xl);
    font-weight: var(--font-weight-bold);
    line-height: var(--leading-tight);
    text-wrap: balance;
}

/* The headline is the link, and the link covers the card. The ::after overlay is
   what makes the whole card clickable without nesting anything inside the
   anchor — so the accessible name stays the headline rather than becoming the
   card's entire text content read out in one breath. */
.blog-card__link {
    color: inherit;
    text-decoration: none;
}

.blog-card__link::after {
    content: "";
    position: absolute;
    inset: 0;
}

.blog-card__link:hover {
    text-decoration: underline;
}

.blog-card__excerpt {
    margin: 0;
    color: var(--color-muted);
    line-height: var(--leading-relaxed);
}

.blog-card__meta {
    margin-top: auto;
    padding-top: 1rem;
    font-size: var(--font-size-sm);
    color: var(--color-muted);
}

/* -- Pager ---------------------------------------------------------------- */

.blog-pager {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    margin-top: 3rem;
}

.blog-pager__page {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* SC 2.5.8 asks for 24px; a control standing on its own gets the app's own
       44px target instead. A row of page numbers is exactly the case the
       success criterion was written about. */
    min-width: var(--touch-target-min);
    min-height: var(--touch-target-min);
    padding: 0 0.75rem;
    border: var(--border-width-hairline) solid var(--color-border);
    border-radius: var(--radius-control-sm);
    color: var(--color-foreground);
    font-weight: var(--font-weight-medium);
    text-decoration: none;
}

.blog-pager__page:hover {
    border-color: var(--color-primary-interactive);
}

/* The current page is marked three ways on purpose: the fill, the weight, and
   aria-current in the markup. Colour alone would fail SC 1.4.1, and a reader
   using a high-contrast mode that flattens the fill still has the weight. */
.blog-pager__page[aria-current="page"] {
    border-color: var(--color-primary-interactive);
    background: var(--color-primary-surface);
    font-weight: var(--font-weight-bold);
}

/* -- Article -------------------------------------------------------------- */

.blog-article__header {
    margin-bottom: 2.5rem;
}

.blog-article__meta {
    margin-top: 1rem;
    font-size: var(--font-size-sm);
    color: var(--color-muted);
}

.blog-article__hero {
    display: block;
    width: 100%;
    margin: 2rem 0;
    border-radius: var(--radius-card);
}

/* The answer-first block. It is a real <aside> with its own heading, not a
   styled paragraph, so a machine lifting "what does this article say" finds a
   labelled region rather than having to guess that the first paragraph is the
   summary. See BlogPostTranslation.AnswerSummary. */
.blog-answer {
    margin: 0 0 2.5rem;
    padding: 1.5rem;
    border-left: var(--border-width-emphasis) solid var(--color-primary-interactive);
    border-radius: var(--radius-sm);
    background: var(--color-primary-surface);
}

.blog-answer__heading {
    margin: 0 0 0.5rem;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-bold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
}

.blog-answer__body {
    margin: 0;
    line-height: var(--leading-relaxed);
}

/* -- Rendered Markdown ---------------------------------------------------- */

/* Everything under .blog-prose is author-written HTML produced by
   Website/MarkdownRenderer, so it cannot carry classes of its own — the
   selectors have to be element selectors, and they are scoped to this one
   container so they touch nothing else in the application. */
.blog-prose {
    font-size: var(--font-size-lg);
    line-height: var(--leading-relaxed);
}

.blog-prose > * + * {
    margin-top: 1.25rem;
}

.blog-prose h2 {
    margin-top: 3rem;
    font-size: var(--font-size-2xl);
    font-weight: var(--font-weight-extrabold);
    letter-spacing: var(--tracking-tight);
    line-height: var(--leading-tight);
}

.blog-prose h3 {
    margin-top: 2.25rem;
    font-size: var(--font-size-xl);
    font-weight: var(--font-weight-bold);
    line-height: var(--leading-tight);
}

.blog-prose a {
    color: var(--color-primary-readable);
    text-underline-offset: 0.2em;
}

.blog-prose ul,
.blog-prose ol {
    padding-left: 1.5rem;
}

.blog-prose li + li {
    margin-top: 0.5rem;
}

.blog-prose blockquote {
    margin-left: 0;
    margin-right: 0;
    padding-left: 1.25rem;
    border-left: var(--border-width-emphasis) solid var(--color-border);
    color: var(--color-muted);
}

.blog-prose code {
    padding: 0.15em 0.4em;
    border-radius: var(--radius-sm);
    background: var(--color-surface-light);
    font-family: var(--font-family-mono);
    font-size: 0.9em;
}

.blog-prose pre {
    padding: 1.25rem;
    overflow-x: auto;
    border-radius: var(--radius-card);
    background: var(--color-surface-light);
}

.blog-prose pre code {
    padding: 0;
    background: none;
}

/* A table wider than the measure scrolls inside its own box. Without this the
   whole page scrolls sideways, which on a phone moves the article text out from
   under the reader as well. */
.blog-prose .table-wrap,
.blog-prose table {
    display: block;
    max-width: 100%;
    overflow-x: auto;
    border-collapse: collapse;
}

.blog-prose th,
.blog-prose td {
    padding: 0.5rem 0.75rem;
    border: var(--border-width-hairline) solid var(--color-border);
    text-align: left;
}

.blog-prose img {
    max-width: 100%;
    height: auto;
    border-radius: var(--radius-card);
}

/* == Markdown editor ====================================================== */

.md-editor {
    border: var(--border-width-hairline) solid var(--color-border);
    border-radius: var(--radius-card);
    background: var(--color-surface);
}

.md-editor__toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.25rem;
    padding: 0.5rem;
    border-bottom: var(--border-width-hairline) solid var(--color-border);
}

.md-editor__panes {
    display: grid;
    grid-template-columns: 1fr;
    gap: 0;
}

/* Side by side only where there is room for two readable measures. Below that
   the preview stacks under the input rather than becoming two unreadable
   columns — the same reason .blog-grid has explicit breakpoints. */
@media (min-width: 64rem) {
    .md-editor__panes--split {
        grid-template-columns: 1fr 1fr;
    }

    .md-editor__panes--split .md-editor__preview {
        border-top: none;
        border-left: var(--border-width-hairline) solid var(--color-border);
    }
}

.md-editor__pane {
    padding: 1rem;
    min-width: 0;
}

.md-editor__preview {
    border-top: var(--border-width-hairline) solid var(--color-border);
    max-height: 40rem;
    overflow-y: auto;
}

.md-editor__label {
    display: block;
    margin: 0 0 0.5rem;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-semibold);
    color: var(--color-muted);
}

.md-editor__input {
    display: block;
    width: 100%;
    padding: 0.75rem;
    border: var(--border-width-hairline) solid var(--color-border);
    border-radius: var(--radius-control-sm);
    background: var(--color-background);
    color: var(--color-foreground);
    font-family: var(--font-family-mono);
    font-size: var(--font-size-sm);
    line-height: var(--leading-relaxed);
    resize: vertical;
}

/* The app-wide ring rule in this file applies to :focus-visible; a textarea is
   restated here because its own border would otherwise be the only indication,
   and a 1px hairline is well under the 3:1 that SC 1.4.11 asks of the boundary
   that identifies a focused control. */
.md-editor__input:focus-visible {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
}

.md-editor__helper {
    margin: 0.5rem 0 0;
    font-size: var(--font-size-sm);
    color: var(--color-muted);
}

/* The preview renders .blog-prose, which is sized for an article page. Inside
   the editor it sits in half a column, so it steps back down to body size. */
.md-editor__preview .blog-prose {
    font-size: var(--font-size-base);
}

.md-editor__preview .blog-prose h2 {
    margin-top: 1.5rem;
    font-size: var(--font-size-xl);
}

.md-editor__preview .blog-prose h3 {
    margin-top: 1.25rem;
    font-size: var(--font-size-lg);
}

/* --------------------------------------------------------------------------
   Projekte board: one column per work item status.

   auto-fit rather than a fixed four, so the board is four columns on a desk,
   two on a tablet and one stacked list on a phone without a breakpoint of its
   own — and never a horizontal scroll, which a board of fixed-width columns
   would force at 400px.

   The columns are told apart by their headings. Nothing here colours a column,
   so the status is never carried by hue alone (SC 1.4.1).
   -------------------------------------------------------------------------- */
.app-board {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 1rem;
    align-items: start;
}

/* An empty column must still be a place to drop onto. */
.app-board-zone {
    min-height: 4rem;
    /* The zone's gap-2, for the drop line — see "Drag and drop" below. */
    --app-drop-gap: 8px;
    --app-drop-reach: 2.75rem;
}

/* The card takes the surface colour, not the column's: the outlined column and
   the outlined card are then two boundaries against two different grounds,
   each of which the palette's outline colour is measured against. */
.app-board-card {
    background-color: var(--mud-palette-surface);
}

/* --------------------------------------------------------------------------
   Drag and drop — every MudDropContainer carrying .app-sortable
   --------------------------------------------------------------------------
   The handle is the only place a drag starts, so it is sized as a control
   standing on its own: --touch-target-min tall, and wide enough for a thumb.
   At the icon's own 20px it missed SC 2.5.8 and was a matter of luck on a
   phone. touch-action is set by MudBlazor on the handle, which is what stops
   the dialog scrolling instead of the row moving. */
.app-drag-handle {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    min-width: 2.25rem;
    min-height: var(--touch-target-min);
    border-radius: var(--mud-default-borderradius);
    color: var(--mud-palette-text-secondary);
    cursor: grab;
}

.app-drag-handle:hover {
    background-color: var(--mud-palette-action-default-hover);
    color: var(--mud-palette-text-primary);
}

.app-drag-handle:active {
    cursor: grabbing;
}

/* The row left behind while its image follows the pointer — see drag-drop.js. */
.app-drag-source {
    opacity: 0.4;
}

/* Where the row will land. MudBlazor's own placeholder is a dashed box some
   50px tall, inserted into the list: every row below jumps down by that much,
   a different row is then under the pointer, the placeholder moves to it, and
   the list shakes while the user is trying to aim. Here it takes up no room at
   all — a line drawn in the gap between two rows — so nothing moves until the
   drop. The margin gives back the one extra flex gap a zero-height child adds. */
.app-sortable .mud-dropitem-placeholder {
    position: relative;
    height: 0;
    margin: 0 0 calc(var(--app-drop-gap, 0px) * -1);
    padding: 0 !important;
    border: 0 !important;
    background: transparent !important;
}

.app-sortable .mud-dropitem-placeholder::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    top: calc(var(--app-drop-gap, 0px) / -2 - 4px);
    height: 4px;
    border-radius: 2px;
    background-color: var(--mud-palette-primary);
    pointer-events: none;
}

/* Which gap the line is drawn in. MudBlazor's rule is "below the row the pointer
   is over", wherever on that row it is. So a row dragged upwards onto the middle
   of another landed underneath it, one place lower than aimed, and the top of
   the list could only be reached through a 20px strip nobody can see.

   While a drag is under way (.app-dragging, set by drag-drop.js) every row
   therefore also answers for the upper part of the row after it, through a
   transparent ::after laid over that part. Over the upper part of a row the line
   is then above it, over the rest below it: the nearest boundary. The strip that
   stands for "before the first row" grows to the same reach.

   Only during a drag: the rest of the time these would sit on top of the next
   row's buttons. --app-drop-reach is about half a row. */
.app-dragging .app-sortable .mud-drop-item:not(.mud-drop-item-preview-start)::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    top: 100%;
    height: calc(var(--app-drop-reach, 2.25rem) + var(--app-drop-gap, 0px));
}

/* Stacked above the row after it by drag-drop.js, which numbers the rows. */
.app-dragging .app-sortable .mud-drop-item {
    position: relative;
}

/* No transition on a row: its stacking and the strip's height have to be in
   force on the very next pointer move, and a transition — even the near-zero
   one the reduced-motion rule leaves on everything — only lands a frame later,
   or never while the tab is not being painted. */
.app-sortable .mud-drop-item {
    transition: none !important;
}

.app-dragging .app-sortable .mud-drop-item-preview-start {
    position: absolute;
    height: var(--app-drop-reach, 2.25rem) !important;
}

/* The column a card is about to move into. An outline, so the column's own
   border and the cards inside it keep the grounds their contrast was measured on. */
.app-drop-target {
    outline: 2px dashed var(--mud-palette-primary);
    outline-offset: 4px;
    border-radius: var(--mud-default-borderradius);
}

/* --------------------------------------------------------------------------
   Refresh — see RefreshButton.razor
   --------------------------------------------------------------------------
   Text for a screen reader only. Clipped rather than display:none, because a
   display:none live region is not in the accessibility tree and announces
   nothing. */
.app-visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* --------------------------------------------------------------------------
   Colour field and colour wheel — see ColorField.razor, ColorWheelPanel.razor
   -------------------------------------------------------------------------- */
.app-color-field {
    display: flex;
    align-items: flex-end;
    gap: 0.25rem;
}

.app-color-field > .mud-input-control {
    flex: 1 1 auto;
    min-width: 0;
}

/* A full touch target around a smaller dot. */
.app-color-swatch {
    flex: none;
    display: grid;
    place-items: center;
    inline-size: var(--touch-target-min);
    block-size: var(--touch-target-min);
    padding: 0;
    border: 0;
    border-radius: var(--radius-full);
    background: none;
    color: var(--color-foreground);
    cursor: pointer;
}

/* The ring is what makes this read as a control: it holds 3:1 against the
   surface in both themes, where the fill is whatever the user chose — white on
   a white dialog included. */
.app-color-swatch-dot,
.app-color-preview {
    display: grid;
    place-items: center;
    inline-size: 1.75rem;
    block-size: 1.75rem;
    border: 2px solid var(--color-tertiary);
    border-radius: var(--radius-full);
}

.app-color-panel {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    inline-size: 18rem;
    max-inline-size: calc(100vw - 2rem);
    padding: 1rem;
}

.app-color-wheel {
    position: relative;
    align-self: center;
    inline-size: 12.5rem;
    block-size: 12.5rem;
    border-radius: var(--radius-full);
    /* Dragged, so the browser must not claim the gesture for scrolling first. */
    touch-action: none;
    cursor: crosshair;
}

.app-color-wheel-canvas {
    display: block;
    inline-size: 100%;
    block-size: 100%;
}

/* White ring inside a dark one: one of the two shows on any colour under it. */
.app-color-wheel-marker {
    position: absolute;
    inset-block-start: -0.5rem;
    inset-inline-start: -0.5rem;
    inline-size: 1rem;
    block-size: 1rem;
    border: 2px solid #fff;
    border-radius: var(--radius-full);
    box-shadow: 0 0 0 1.5px #000, inset 0 0 0 1px #000;
    pointer-events: none;
}

.app-color-slider {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

.app-color-slider input[type="range"] {
    inline-size: 100%;
    block-size: 1.5rem;
    margin: 0;
    accent-color: var(--mud-palette-primary);
}

.app-color-format {
    display: flex;
    align-items: flex-end;
    gap: 0.75rem;
}

.app-color-preview {
    flex: none;
    margin-block-end: 0.25rem;
}

.app-color-channels {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.5rem;
}
