/* Shared GUI - Common App Styles (MudBlazor best practices)
 *
 * ⚠️ STOP — Before adding CSS here, check if MudBlazor utility classes cover it.
 * Reference: docs/reference/mudblazor-css-utilities.md
 *
 * MudBlazor provides utility classes for: display, flex, gap, spacing,
 * overflow, position, visibility, z-index, border, cursor, pointer-events.
 * Apply these directly in Razor markup (e.g., class="d-flex gap-2 pa-4").
 *
 * Only add custom CSS here for:
 *   - Theme-aware overrides (scrollbars, dialog title)
 *   - Third-party library integration (SortableJS)
 *   - Complex layouts (CSS Grid, container queries)
 *   - MudBlazor internal overrides (.mud-table-toolbar)
 *   - Animation/transitions
 */

/* =============================================================================
   CUSTOM SCROLLBARS
   ============================================================================= */

/* Theme-aware custom scrollbars */
/* Firefox */
html {
  scrollbar-width: thin;
  scrollbar-color: var(--mud-palette-text-secondary, #888) var(--mud-palette-background, #f1f1f1);
}

/* Chromium/Edge/Safari */
::-webkit-scrollbar { width: 8px; height: auto; }
::-webkit-scrollbar-corner { background: var(--mud-palette-background, #f1f1f1); }

/* Light mode */
:root[data-theme='light'] ::-webkit-scrollbar-track { background: var(--mud-palette-background, #f1f1f1); }
:root[data-theme='light'] ::-webkit-scrollbar-thumb { background: var(--mud-palette-text-secondary, #888); border-radius: 4px; }
:root[data-theme='light'] ::-webkit-scrollbar-thumb:hover { background: var(--mud-palette-text-primary, #555); }

/* Dark mode */
:root[data-theme='dark'] ::-webkit-scrollbar-track { background: var(--mud-palette-surface, #222); }
:root[data-theme='dark'] ::-webkit-scrollbar-thumb { background: var(--mud-palette-text-secondary, #bbb); border-radius: 4px; }
:root[data-theme='dark'] ::-webkit-scrollbar-thumb:hover { background: var(--mud-palette-text-primary, #fff); }

/* =============================================================================
   SORTABLE COMPONENTS - SortableJS drag-and-drop styles
   ============================================================================= */

/* Drag handle styling */
.sortable-handle {
    cursor: grab;
    touch-action: none;
    display: flex;
    align-items: center;
    padding: 4px;
    color: var(--mud-palette-text-secondary);
    transition: color 0.15s ease;
}

.sortable-handle:hover {
    color: var(--mud-palette-text-primary);
}

.sortable-handle:active {
    cursor: grabbing;
}

/* Ghost element (drop placeholder) */
.sortable-ghost {
    opacity: 0.4;
    background-color: var(--mud-palette-primary-lighten) !important;
    border: 2px dashed var(--mud-palette-primary) !important;
    border-radius: 4px;
}

/* Chosen/selected item being dragged */
.sortable-chosen {
    background-color: var(--mud-palette-background-gray);
}

/* Item while actively dragging */
.sortable-drag {
    background-color: var(--mud-palette-surface);
    box-shadow: var(--mud-elevation-4);
    border-radius: 4px;
}

/* Tri-state filter chip: EXCLUDE strikes the label through — together with the Block icon this
   says "NOT this value" instead of the old X, which read as "remove this chip". */
.aivora-filter-chip--exclude .mud-chip-content {
    text-decoration: line-through;
}

/* Sortable list container */
.aivora-sortable-list {
    display: flex;
    flex-direction: column;
}

/* Individual sortable item — flex row with handle + content */
.sortable-item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    background-color: var(--mud-palette-surface);
    border-bottom: 1px solid var(--mud-palette-lines-default);
    transition: background-color 0.15s ease;
}

.sortable-item:last-child {
    border-bottom: none;
}

.sortable-item:hover {
    background-color: var(--mud-palette-action-default-hover);
}

/* Item content area (takes remaining space) */
.sortable-item-content {
    flex: 1;
    min-width: 0;
}

/* Disabled state */
.aivora-sortable-list--disabled .sortable-handle {
    cursor: default;
    opacity: 0.4;
    pointer-events: none;
}

.aivora-sortable-list--disabled .sortable-item {
    opacity: 0.7;
}

/* Horizontal orientation — items lay out left-to-right and drag horizontally (e.g. column strip).
   Only the layout axis changes; the divider moves from the bottom edge to the trailing edge. */
.aivora-sortable-list--horizontal {
    flex-direction: row;
    flex-wrap: nowrap;
    overflow-x: auto;
}

.aivora-sortable-list--horizontal .sortable-item {
    border-bottom: none;
    border-right: 1px solid var(--mud-palette-lines-default);
}

.aivora-sortable-list--horizontal .sortable-item:last-child {
    border-right: none;
}

/* In a row, an item sizes to its content rather than stretching full width. */
.aivora-sortable-list--horizontal .sortable-item-content {
    flex: 0 0 auto;
}

/* =============================================================================
   PROPERTY GRID - AttributeSet property list (CSS Grid + AivoraSortableList)
   Desktop: 5-column grid (table-like)  |  Narrow: single-column card stack
   Uses CSS Container Queries so the layout responds to the PANEL width,
   not the viewport — critical when sharing screen with the tree panel.
   ============================================================================= */

/* Container context — the property grid section responds to its own width */
.property-grid-container {
    container-name: property-grid;
    container-type: inline-size;
}

/* Shared grid layout used by header, data rows, and add row */
.property-grid-row {
    display: grid;
    grid-template-columns: 1fr 120px 110px 150px 70px;
    gap: 8px;
    align-items: center;
}

/* Header row — mirrors sortable-item flex layout for column alignment */
.property-grid-header-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    border-bottom: 2px solid var(--mud-palette-lines-default);
}

.property-grid-header-row .property-grid-row {
    flex: 1;
    min-width: 0;
    font-weight: 600;
    font-size: 0.8125rem;
    color: var(--mud-palette-text-secondary);
}

/* A field's ▸ panel under its row (field-rules FR3): the settings beside the 👁 preview; stacked when narrow. */
.field-settings-panel {
    display: grid;
    grid-template-columns: 1.3fr 1fr;
    gap: 16px;
    margin: 4px 0 8px;
    padding: 12px;
    border-radius: 8px;
    background: var(--mud-palette-background-gray);
}

/* The rule's summary reads as a sentence, not as a shouted button label. */
.mud-button-root.field-rule-toggle,
.field-rule-toggle .mud-button-label {
    text-transform: none;
}

.field-settings-panel-boxes {
    display: flex;
    gap: 8px;
    align-items: center;
}

/* The line under a field (field-rules FR4): its description on the left, a Text box's counter on the right. */
.field-help-line {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    margin-top: 2px;
}

.field-help-counter {
    white-space: nowrap;
}

.field-rule-preview {
    border: 1px dashed var(--mud-palette-primary);
    border-radius: 8px;
    padding: 10px;
}

/* Reorder spinner — matches drag handle icon size */
.property-reorder-spinner {
    width: 20px;
    height: 20px;
}

/* Invisible spacer — same width as .sortable-handle (4px pad + 20px icon + 4px pad) */
.property-grid-handle-spacer {
    width: 28px;
    flex-shrink: 0;
}

/* Read-only handle filler — the row renders the SAME drag icon as the editable row but visibility:hidden,
   so the reserved width tracks the real icon automatically (no magic number) and the read-only row stays
   column-aligned with the header's handle spacer. */
.property-grid-handle-hidden {
    visibility: hidden;
}

/* Add row — below sortable list, not draggable */
.property-grid-add-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    border-top: 1px dashed var(--mud-palette-lines-default);
    margin-top: 4px;
}

.property-grid-add-row .property-grid-row {
    flex: 1;
    min-width: 0;
}

/* Mobile field label — hidden when wide, shown when narrow */
.property-grid-label {
    display: none;
}

/* Header wrapper — hidden when narrow (container query handles this) */
.property-grid-header-wrapper {
    display: block;
}

/* ---- Narrow layout: container query (responds to PANEL width) ---- */
@container property-grid (max-width: 500px) {
    .property-grid-header-wrapper {
        display: none;
    }

    .field-settings-panel {
        grid-template-columns: 1fr;
    }

    .property-grid-row {
        grid-template-columns: 1fr;
    }

    .property-grid-label {
        display: block;
        font-size: 0.75rem;
        color: var(--mud-palette-text-secondary);
        margin-bottom: 2px;
    }

    .property-grid-add-row {
        flex-direction: column;
        align-items: stretch;
    }

    .property-grid-add-row .property-grid-row {
        grid-template-columns: 1fr;
    }

    .property-grid-add-row .property-grid-add-btn {
        align-self: flex-end;
    }
}

/* ---- Fallback: viewport media query for full-width scenarios ---- */
/* Covers phones where the panel IS the viewport (no container query ancestor) */
@media (max-width: 599.98px) {
    .property-grid-header-wrapper {
        display: none;
    }

    .field-settings-panel {
        grid-template-columns: 1fr;
    }

    .property-grid-row {
        grid-template-columns: 1fr;
    }

    .property-grid-label {
        display: block;
        font-size: 0.75rem;
        color: var(--mud-palette-text-secondary);
        margin-bottom: 2px;
    }

    .property-grid-add-row {
        flex-direction: column;
        align-items: stretch;
    }

    .property-grid-add-row .property-grid-row {
        grid-template-columns: 1fr;
    }

    .property-grid-add-row .property-grid-add-btn {
        align-self: flex-end;
    }
}

/* =============================================================================
   TREE VIEW STYLES (AivoraSortableTreeView)
   ============================================================================= */

.aivora-sortable-tree {
    display: flex;
    flex-direction: column;
}

/* Node header row — entire row is clickable for selection.
   Small horizontal margin insets the row so the rounded background/selection
   highlight reads as a pill instead of touching the panel edge. */
.tree-node-header {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 8px 12px;
    margin: 0 4px;
    min-height: 48px;
    cursor: pointer;
    border-radius: var(--mud-default-borderradius);
    transition: background-color 0.15s ease;
}

.tree-node-header:hover {
    background-color: var(--mud-palette-action-default-hover);
}

/* Tree drag handle */
.tree-handle {
    display: flex;
    align-items: center;
    padding: 4px;
    touch-action: none;
    color: var(--mud-palette-text-secondary);
    opacity: 0.6;
    transition: opacity 0.15s ease, color 0.15s ease;
}

.aivora-sortable-tree--disabled .tree-handle,
.aivora-sortable-tree--disabled .tree-handle * {
    cursor: default !important;
}

.aivora-sortable-tree--disabled .tree-node:hover .tree-handle {
    opacity: 0.6;
}

.aivora-sortable-tree--disabled .tree-handle:hover {
    color: var(--mud-palette-text-secondary);
}

.tree-node:hover .tree-handle {
    opacity: 1;
}

.tree-handle:hover {
    color: var(--mud-palette-text-primary);
}

/* Tree handle disabled (individual handles, not whole tree) */
.tree-handle--disabled {
    cursor: default !important;
    pointer-events: none;
    opacity: 0.4;
}

.tree-handle--disabled * {
    cursor: default !important;
}

/* Expand/collapse button; the invisible leaf spacer shares this class for flex-shrink. */
.tree-expand-btn {
    flex-shrink: 0;
}

/* Leaf rows render an invisible Size.Small MudIconButton (.tree-expand-btn.tree-expand-spacer)
   as their expand slot, so its width matches the real chevron button by construction —
   keeping leaf icons aligned with their folder/member siblings without a hardcoded width. */
.tree-expand-spacer {
    visibility: hidden;
}

/* Node content area */
.tree-node-content {
    flex: 1;
    min-width: 0;
}

/* Handle on right side - adjust padding for depth indent */
.tree-node-header--handle-right {
    padding-right: 4px; /* Reduced padding on right when handle is there */
}

/* Children container */
.tree-children-container {
    min-height: 4px; /* Ensure drop target exists even when empty */
}

/* Drop target feedback */
.tree-children-container.tree-drop-target--active {
    outline: 2px dashed var(--mud-palette-primary);
    outline-offset: 2px;
    border-radius: var(--mud-default-borderradius);
    background-color: color-mix(in srgb, var(--mud-palette-primary) 8%, transparent);
}

/* Root drop target feedback should be subtle (avoid large dotted outline around the entire tree) */
.tree-children-container.tree-drop-target--active-root {
    outline: none;
    background-color: color-mix(in srgb, var(--mud-palette-primary) 4%, transparent);
}

.tree-children-container.tree-drop-target--invalid {
    outline-color: var(--mud-palette-error);
    background-color: color-mix(in srgb, var(--mud-palette-error) 10%, transparent);
}

.tree-children-container.tree-drop-target--invalid-root {
    outline: none;
    background-color: color-mix(in srgb, var(--mud-palette-error) 6%, transparent);
}

/* Folder header highlight (shows what folder you're dropping into) */
.tree-node-header.tree-drop-target-header--active {
    background-color: color-mix(in srgb, var(--mud-palette-primary) 10%, var(--mud-palette-action-default-hover));
    box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--mud-palette-primary) 65%, transparent);
}

.tree-node-header.tree-drop-target-header--invalid {
    background-color: color-mix(in srgb, var(--mud-palette-error) 10%, var(--mud-palette-action-default-hover));
    box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--mud-palette-error) 70%, transparent);
}

/* Empty states */
.tree-empty {
    text-align: center;
    color: var(--mud-palette-text-secondary);
    padding: 16px;
}

.tree-empty-folder {
    padding: 8px 12px;
    color: var(--mud-palette-text-secondary);
    font-style: italic;
}

/* Folder action buttons - rendered first inside folder's children container */
.tree-folder-actions {
    padding: 4px 12px 8px 12px;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

/* Selected node — primary tint background + left accent bar */
.tree-node--selected > .tree-node-header {
    background-color: color-mix(in srgb, var(--mud-palette-primary) 12%, var(--mud-palette-surface));
    box-shadow: inset 3px 0 0 0 var(--mud-palette-primary);
}

.tree-node--selected > .tree-node-header:hover {
    background-color: color-mix(in srgb, var(--mud-palette-primary) 18%, var(--mud-palette-surface));
    box-shadow: inset 3px 0 0 0 var(--mud-palette-primary);
}

/* Keyboard focus — a DIFFERENT state from selected, and it has to look different.
   Selection does not follow focus in this tree (selecting loads a whole editor), so the focused row
   and the selected row are routinely two different rows. Selected is a tint plus a left bar; focused
   is a ring. A row that is both shows both. The row itself carries the tabindex, so the ring is drawn
   on the row and inset onto the header, which is the part a person sees as the row. */
.tree-node:focus {
    outline: none;
}

.tree-node:focus-visible > .tree-node-header {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
    border-radius: var(--mud-default-borderradius);
}

/* Holding the move modifier (Ctrl or Shift) changes what the arrows will do, so the row says so
   BEFORE one is pressed — while the person still has the choice not to press it. A dashed edge
   because the row is about to be picked up, not because anything is selected or wrong. */
.tree-node--move-armed > .tree-node-header {
    outline: 2px dashed var(--mud-palette-warning);
    outline-offset: -2px;
    border-radius: var(--mud-default-borderradius);
    background-color: color-mix(in srgb, var(--mud-palette-warning) 12%, var(--mud-palette-surface));
}

/* A kind that does not move says that in the same breath, muted rather than red: nothing has gone
   wrong — this row simply is not one that moves, and red would report a failure that did not happen. */
.tree-node--move-blocked > .tree-node-header {
    outline: 2px dashed var(--mud-palette-text-disabled);
    outline-offset: -2px;
    border-radius: var(--mud-default-borderradius);
    background-color: var(--mud-palette-action-default-hover);
    cursor: not-allowed;
}

/* A row the search term matched. A tint and no outline, because the outlines are already
   spoken for: a solid one is keyboard focus and a dashed one means the arrows are about to
   move this row. Info rather than primary, which is what SELECTED already uses. */
.tree-node--search-match > .tree-node-header {
    background-color: color-mix(in srgb, var(--mud-palette-info) 12%, var(--mud-palette-surface));
    border-radius: var(--mud-default-borderradius);
}

/* The one match Enter is standing on. It has to read stronger than the others: while the person
   is still typing, the DOM focus is in the box on purpose, so no focus ring marks the place. */
.tree-node--search-current > .tree-node-header {
    background-color: color-mix(in srgb, var(--mud-palette-info) 30%, var(--mud-palette-surface));
    border-radius: var(--mud-default-borderradius);
}

/* The find-a-row box above the tree. The count keeps its own width so the field does not
   jump a few pixels sideways on every keystroke as the digits change. */
.aivora-tree-search {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
}

.aivora-tree-search__field {
    flex: 1 1 auto;
    min-width: 0;
}

.aivora-tree-search__status {
    flex: 0 0 auto;
    min-width: 4.5rem;
    text-align: right;
}

/* The SERVER search's result list. A list rather than tints in the tree, because a lazily-loaded
   tree has never seen most of these rows - there is nothing on screen to tint. */
.aivora-tree-search-results {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: var(--mud-default-borderradius);
    margin-bottom: 8px;
    max-height: 320px;
    overflow-y: auto;
}

.aivora-tree-search-result {
    padding: 6px 10px;
    cursor: pointer;
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.aivora-tree-search-result:last-child {
    border-bottom: none;
}

.aivora-tree-search-result:hover,
.aivora-tree-search-result:focus-visible {
    background-color: var(--mud-palette-action-default-hover);
    outline: none;
}

/* The keyboard's own mark. The hover tint alone would leave a person tabbing through the list
   with no idea where they are. */
.aivora-tree-search-result:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
}

.aivora-tree-search-result__name {
    font-size: 0.875rem;
}

/* The path is what replaces seeing the row where it sits, so it is quieter than the name but
   never hidden - it is the only thing telling two products with one name apart. */
.aivora-tree-search-result__path {
    font-size: 0.75rem;
    color: var(--mud-palette-text-secondary);
}

.aivora-tree-search__truncated {
    margin-bottom: 8px;
}

/* SortableJS states for tree nodes */
.tree-node.sortable-ghost {
    opacity: 0.4;
    background-color: var(--mud-palette-primary-lighten) !important;
    border: 2px dashed var(--mud-palette-primary) !important;
    border-radius: var(--mud-default-borderradius);
}

.tree-node.sortable-chosen {
    background-color: var(--mud-palette-background-gray);
    box-shadow: var(--mud-elevation-2);
    border-radius: var(--mud-default-borderradius);
}

.tree-node.sortable-drag {
    background-color: var(--mud-palette-surface);
    box-shadow: var(--mud-elevation-4);
    border-radius: var(--mud-default-borderradius);
}

/* Fallback element (transparent clone following cursor) needs relative positioning for indicator */
.sortable-fallback {
    position: relative;
}

/* Drop target indicator - shows folder name or breadcrumb during drag */
/* Appended inside .sortable-fallback, positioned above it */
.sortable-drop-indicator {
    position: absolute;
    bottom: 100%; /* Position above parent - works regardless of content height */
    left: 0;
    margin-bottom: 4px; /* Gap between indicator and fallback element */
    max-width: min(400px, calc(100vw - 32px));
    overflow: hidden;
    text-overflow: ellipsis;
    direction: rtl; /* Truncate from LEFT, keeping destination folder visible */
    background: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-primary);
    border-radius: var(--mud-default-borderradius);
    padding: 2px 8px;
    font-size: 12px;
    font-weight: 500;
    color: var(--mud-palette-primary);
    box-shadow: var(--mud-elevation-4);
    pointer-events: none;
    z-index: 10000;
    white-space: nowrap;
    display: none;
}

/* Multi-line drop indicator (with position info) */
.sortable-drop-indicator--multiline {
    white-space: normal;
    text-align: center;
    direction: ltr; /* Normal text direction for multi-line */
}

.sortable-drop-indicator__folder {
    display: block;
    direction: rtl; /* Truncate from left for breadcrumb */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.sortable-drop-indicator__position {
    display: block;
    font-size: 11px;
    font-weight: 400;
    color: var(--mud-palette-text-secondary);
    margin-top: 2px;
    direction: ltr; /* Normal direction for position */
}

/* Tree disabled state */
.aivora-sortable-tree--disabled .tree-handle {
    opacity: 0.4;
    pointer-events: none;
}

.aivora-sortable-tree--disabled .tree-node {
    opacity: 0.7;
}

/* Utility class for cursor */
.cursor-grab {
    cursor: grab;
}

.cursor-grab:active {
    cursor: grabbing;
}

/* ========================================================================
   Virtualized DataGrid — fixed row height
   ========================================================================
   Blazor's Virtualize component requires ALL rows to have identical height
   matching the declared ItemSize. If any row wraps or grows, the spacer
   math breaks and Virtualize oscillates between start indices (visible as
   "jumping" scroll). white-space:nowrap prevents line wrapping so every
   row stays single-line. overflow:hidden clips content that exceeds the
   column width (managed by MudDataGrid's column resize feature).
   Do NOT add max-width:0 — it breaks column resizing.
   See: docs/reference/blazor-virtualize-server-data-pattern.md
   ======================================================================== */
.audit-grid .mud-table-container .mud-table-row .mud-table-cell {
    white-space: nowrap;
    overflow: hidden;
}

/* ========================================================================
   AivoraDialog — Branded title bar
   Applied via MudDialog's TitleClass parameter.
   ======================================================================== */
.aivora-dialog-title {
    background-color: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
}

.aivora-dialog-title .mud-typography,
.aivora-dialog-title .mud-button-root,
.aivora-dialog-title .mud-icon-root {
    color: inherit;
}

/* Draggable title bar — cursor indicates drag capability */
.aivora-dialog-title--draggable {
    cursor: grab;
    user-select: none;
    -webkit-user-select: none;
    touch-action: none;
}

/* Active drag state — grabbing cursor */
.aivora-dialog-title--dragging {
    cursor: grabbing;
}

/* Ensure buttons inside title bar remain clickable with default cursor */
.aivora-dialog-title--draggable button,
.aivora-dialog-title--draggable a,
.aivora-dialog-title--draggable [role="button"] {
    cursor: pointer;
}

/* =============================================================================
   BROWSER AUTOFILL — Consistent styling for autofilled inputs
   =============================================================================
   Problem: Browsers inject their own background color on autofilled <input>
   elements (e.g., light blue in Chrome, yellow in Firefox). MudBlazor's
   adornment container (eye icon, clear icon) keeps its own background,
   creating a visual disconnect — the adornment appears as a separate
   white box next to the colored input.

   Fix: Theme-aware autofill background + transparent adornment when the
   input wrapper contains an autofilled input. Also hides Edge's native
   password reveal since we use our own toggle (AivoraPasswordField).
   ============================================================================= */

/* ---- Remove Edge native password reveal (we use AivoraPasswordField toggle) ---- */
input[type="password"]::-ms-reveal,
input[type="password"]::-ms-clear {
    display: none;
}

/* ---- Autofill background: override browser default to theme-aware color ---- */
/* The large inset box-shadow trick overrides the UA stylesheet background */
.mud-input-slot:-webkit-autofill,
.mud-input-slot:-webkit-autofill:hover,
.mud-input-slot:-webkit-autofill:focus {
    -webkit-text-fill-color: var(--mud-palette-text-primary);
    -webkit-box-shadow: 0 0 0 1000px var(--mud-palette-background) inset;
    box-shadow: 0 0 0 1000px var(--mud-palette-background) inset;
    transition: background-color 5000s ease-in-out 0s; /* Prevent browser from re-applying its color */
}

/* ---- Adornment stays transparent when sibling input is autofilled ---- */
/* :has() matches the .mud-input wrapper when it contains an autofilled input */
.mud-input:has(.mud-input-slot:-webkit-autofill) .mud-input-adornment {
    background: transparent;
}

/* ---- Firefox autofill (uses :autofill pseudo-class) ---- */
.mud-input-slot:autofill {
    -webkit-text-fill-color: var(--mud-palette-text-primary);
    box-shadow: 0 0 0 1000px var(--mud-palette-background) inset;
}

.mud-input:has(.mud-input-slot:autofill) .mud-input-adornment {
    background: transparent;
}

/* ── Accent "strong" utilities (design-system S8b) ──────────────────────────────
   Aurora's accent colours are vivid FILLS; as an icon/text colour on a plain
   surface the base shade is too light for WCAG AA. MudBlazor has no "darken" Color
   enum, so these map the icon/text colour to the theme's *-darken token (still
   theme-driven, no hardcoded hex). Apply via Class on the icon/text; !important
   beats MudBlazor's mud-{colour}-text. Do NOT use on a filled button/chip (the
   fill's on-colour text already handles contrast). */
.aivora-strong-info { color: var(--mud-palette-info-darken) !important; }
.aivora-strong-success { color: var(--mud-palette-success-darken) !important; }
.aivora-strong-warning { color: var(--mud-palette-warning-darken) !important; }
.aivora-strong-secondary { color: var(--mud-palette-secondary-darken) !important; }

/* Dark mode: the surface is dark, so *darkening* the accent lowers contrast
   (success/warning-darken fall below AA on a dark card). On a dark surface the
   *vivid base* shade is the bright foreground — use it instead of the darken. */
:root[data-theme='dark'] .aivora-strong-info { color: var(--mud-palette-info) !important; }
:root[data-theme='dark'] .aivora-strong-success { color: var(--mud-palette-success) !important; }
:root[data-theme='dark'] .aivora-strong-warning { color: var(--mud-palette-warning) !important; }
:root[data-theme='dark'] .aivora-strong-secondary { color: var(--mud-palette-secondary) !important; }

/* ── App bar background (composed-value token) ──────────────────────────────────
   The app bar reads ONE composed CSS value, --aivora-appbar-background, applied via
   Class="aivora-appbar" on MudAppBar (the Booking `glass-effect` pattern). ONE rule
   serves BOTH modes: the per-mode value (tenant override, or the seeded default) is
   emitted into each mode's :root, and the var() fallback here is the Aurora gradient
   composed from var(--mud-palette-*) refs that resolve per mode — so Light and Dark are
   structurally identical, only the resolved palette values differ. The composer editor
   (AppBarBackgroundNodeEditor) is the sole writer of the value. !important beats
   MudBlazor's own appbar-background token; AppBarText stays white/near-white. The default
   gradient ends on secondary-*darken* so app-bar text/icons clear the 3:1 UI contrast
   floor in both modes. SSOT plan: docs/plans/appbar-background-style.md

   SPECIFICITY: MudAppBar renders Color="Primary" as `.mud-appbar.mud-theme-primary`, whose
   `background-color: var(--mud-palette-primary) !important` is a TWO-class rule. Our `background`
   shorthand implies `background-color: transparent` — essential for Glass, where a translucent
   value must reveal the backdrop. A one-class `.aivora-appbar` LOSES that background-color to the
   two-class theme rule, so an OPAQUE primary layer showed through the alpha and defeated Glass
   (confirmed via computed style: backgroundColor was opaque rgb(99,91,255) while the gradient's
   own stops carried alpha). The repeated class (0,3,0) out-ranks the theme rule so our
   background — transparent color + composed image — wins. backdrop-filter itself works (verified
   live: a translucent test box frosted the content behind it). */
.aivora-appbar.aivora-appbar.aivora-appbar {
    background: var(--aivora-appbar-background, linear-gradient(135deg,
        var(--mud-palette-primary-darken) 0%,
        var(--mud-palette-tertiary-darken) 40%,
        var(--mud-palette-secondary-darken) 100%)) !important;
    /* Glass: a fixed backdrop blur that is INVISIBLE while the background is opaque (Solid/Gradient
       with Glass=0) and reveals the frosted effect as the composer's Glass slider lowers the
       background's alpha. Intensity lives in the single --aivora-appbar-background value (its alpha),
       so no second stored value is needed. See docs/plans/appbar-background-style.md. */
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
}

/* Offline cue must survive in BOTH modes: MudAppBar switches to Color.Warning when the
   connection drops. Out-specifies the base rule above (now 0,3,0) — one more repeated class —
   so the solid amber bar wins while offline. */
.aivora-appbar.aivora-appbar.aivora-appbar.mud-theme-warning {
    background: var(--mud-palette-warning) !important;
}

/* ── Default tooltip contrast (dark mode) ───────────────────────────────────────
   MudBlazor's own rule pairs two tokens that are NOT a colour/on-colour pair:

       .mud-tooltip.mud-tooltip-default {
           color: var(--mud-palette-dark-text);
           background-color: var(--mud-palette-gray-darker);
       }

   `dark-text` is the on-colour for `dark`, not for the grey ramp. The grey ramp is
   INVERTED between modes by design (dark mode's `gray-darker` is its LIGHTEST stop,
   #E0E0E0), so in dark mode the pairing became white-on-near-white — 1.3:1, an
   unreadable tooltip. Light mode happened to survive (#616161, 6.2:1), which is why
   it went unnoticed.

   Fix = pair `dark-text` with its actual partner `dark`: 14.8:1 dark / 10.0:1 light,
   both clear of WCAG AA. ONE rule for both modes, tokens only — a tenant that edits
   the Dark colour keeps a matching tooltip because the pair moves together.

   SPECIFICITY: every host links this file BEFORE MudBlazor.min.css, so an equal
   (0,2,0) rule would lose on source order. The repeated class lifts ours to (0,3,0)
   and wins regardless of order — same trick as .aivora-appbar above, and it keeps
   !important out of it. The arrow needs its own rule: MudBlazor colours the caret
   from `gray-darker` explicitly, so inheriting is not enough. */
.mud-tooltip.mud-tooltip.mud-tooltip-default {
    background-color: var(--mud-palette-dark);
    color: var(--mud-palette-dark-text);
}

.mud-tooltip.mud-tooltip.mud-tooltip-default.mud-tooltip-arrow::after {
    border-top-color: var(--mud-palette-dark);
}

/* ── Default avatar contrast (AivoraAvatar's grey person) ──────────────────────
   Same family as the tooltip above. MudBlazor paints a default filled avatar
   `text-primary` on `lines-inputs`, and in DARK mode `lines-inputs` is 30%
   translucent white — so the person's contrast depends on whatever is behind the
   circle (the top bar's gradient, a card). Its base `.mud-avatar` pair is worse
   still: white on `gray-light`, 1.9:1 in light mode.

   Re-paired with two OPAQUE content tokens, readable wherever the avatar sits:
   `text-secondary` on `background-gray` — 4.3:1 light, 4.0:1 dark (the person is
   a UI graphic, floor 3:1). A picture covers the circle, so only the grey person
   shows these colours. Three classes = (0,3,0), which beats MudBlazor's (0,1,0)
   whatever the load order. Asserted by MudBlazorColourContrastGuardTests. */
.mud-avatar.mud-avatar-filled.mud-avatar-filled-default {
    color: var(--mud-palette-text-secondary);
    background-color: var(--mud-palette-background-gray);
}

/* ── Alert message text (tinted + outlined variants) ────────────────────────────
   Same family as the tooltip above, found by sweeping every stylesheet rule that
   sets a text AND a background token (see DefaultTooltipCssContrastGuardTests for
   the method). MudBlazor writes an alert's MESSAGE in the accent's `-darken` shade:

       .mud-alert-text-{severity}     { color: {sev}-darken; background: {sev}-hover }
       .mud-alert-outlined-{severity} { color: {sev}-darken }

   `{sev}-hover` is a ~6% tint, so the message is effectively accent-on-surface —
   the exact case §2 of colour-usage.md already calls the #1 trap (an accent is a
   FILL, not body text). Measured against the Aurora palette, the default (tinted)
   variant lands at 2.9:1 error / 4.2:1 warning dark and 2.6:1 info / 3.0:1 warning
   light; outlined at 2.8:1 info light / 3.0:1 error dark. Body text needs 4.5:1, and
   93 of our 102 alerts use the default variant, so this was the widest readability
   defect in the app.

   Fix = write the message in `text-primary` and leave the tint, the coloured icon
   and the outlined border alone: 11.9–17:1 in both modes, severity still obvious at
   a glance from the icon and tint. This is not an invention — it is MudBlazor's OWN
   `.mud-alert-text-normal`, which already pairs the tint with `text-primary`; only
   the coloured severities deviated. All four severities are listed because any of
   them can be used at any time (Success is currently used once); omitting two would
   regress the moment someone changes a Severity.

   SPECIFICITY: MudBlazor's rules are single-class (0,1,0) and this file loads first,
   so the repeated class (0,2,0) is what makes ours win. The icon keeps its colour
   without an exception here — `.mud-alert-{variant}-{sev} .mud-alert-icon` targets a
   CHILD element, so it is not in conflict with a colour set on the alert root. */
.mud-alert-text-info.mud-alert-text-info,
.mud-alert-text-success.mud-alert-text-success,
.mud-alert-text-warning.mud-alert-text-warning,
.mud-alert-text-error.mud-alert-text-error,
.mud-alert-outlined-info.mud-alert-outlined-info,
.mud-alert-outlined-success.mud-alert-outlined-success,
.mud-alert-outlined-warning.mud-alert-outlined-warning,
.mud-alert-outlined-error.mud-alert-outlined-error {
    color: var(--mud-palette-text-primary);
}

/* ── Filled ERROR alert: deepen the fill so the sentence clears AA (plan S4) ─────
   The same pair passes or fails depending on what it is used FOR: white on our error
   red is 3.8:1, which clears the 3:1 floor for a big bold BUTTON label but not the
   4.5:1 a body-text sentence needs — and a filled alert is a sentence (3 of them:
   the file / image / encrypted uploaders).

   Deepening only the ALERT's fill to `error-darken` gives 4.8:1 and leaves every
   error button, chip and badge untouched. The alternative — re-valuing the `error`
   token — would change every error surface at once and require re-checking each pair,
   a wide blast radius for three alerts.

   ONLY error is written here. The other filled severities carry DARK on-colour text
   and already pass on their base shade (info 8.3 · success 7.0 · warning 8.3);
   darkening them would LOWER those ratios, so a rule for them would be both dead and
   wrong. MudBlazor's `#blazor-error-ui` shares the pair but none of our hosts render
   that element, so it needs nothing.

   SPECIFICITY: MudBlazor's rule is single-class and this file loads first, so the
   repeated class is what makes ours win. */
.mud-alert-filled-error.mud-alert-filled-error {
    background-color: var(--mud-palette-error-darken);
}

/* ── Accent on its own tint: alert ICON + active nav link ───────────────────────
   The third face of the same defect (plan S5). MudBlazor paints these in the VIVID
   accent on a ~6% tint of that same accent. An icon/state that carries meaning has a
   3:1 floor (WCAG 1.4.11) and a link LABEL you read has 4.5 — measured against Aurora,
   the vivid shade misses both in one mode:

     alert icon,  light   warning 2.0 · info 2.0 · success 2.4 · error 3.5
     alert icon,  dark    3.7–6.2  ✓ already clear — so there is NO dark rule below
     nav link     light 4.3 · dark 3.1   (label = 4.5 floor → short in BOTH modes)

   Fix = the SAME mode-aware move `.aivora-strong-*` already makes: on a light tint use
   the *-darken shade, on a dark tint use the lighter one. Existing tokens only, one
   more consumer of an existing pattern — no new token, and no per-component colour
   judgement. Nothing is written for a mode that already passes: writing a dark rule for
   the alert icon would be dead CSS.

   The tint and the severity cue survive — only the shade of the SAME hue changes.

   SPECIFICITY: MudBlazor's nav rule counts five classes (0,5,0) and this file loads
   first, so `:root[data-theme]` + a repeated class lifts ours past it. */
:root[data-theme='light'] .mud-alert-text-info .mud-alert-icon,
:root[data-theme='light'] .mud-alert-outlined-info .mud-alert-icon {
    color: var(--mud-palette-info-darken);
}

:root[data-theme='light'] .mud-alert-text-success .mud-alert-icon,
:root[data-theme='light'] .mud-alert-outlined-success .mud-alert-icon {
    color: var(--mud-palette-success-darken);
}

:root[data-theme='light'] .mud-alert-text-warning .mud-alert-icon,
:root[data-theme='light'] .mud-alert-outlined-warning .mud-alert-icon {
    color: var(--mud-palette-warning-darken);
}

:root[data-theme='light'] .mud-alert-text-error .mud-alert-icon,
:root[data-theme='light'] .mud-alert-outlined-error .mud-alert-icon {
    color: var(--mud-palette-error-darken);
}

/* Active nav link — the sidebar's current item, the most-seen instance of this defect.
   Only the DEFAULT navmenu colour is written because that is what all 4 of our
   MudNavMenu usages render; a rule for a variant we never render would be dead CSS. */
:root[data-theme='light'] .mud-navmenu.mud-navmenu-default.mud-navmenu-default .mud-nav-link.active:not(.mud-nav-link-disabled) {
    color: var(--mud-palette-primary-darken);
}

:root[data-theme='dark'] .mud-navmenu.mud-navmenu-default.mud-navmenu-default .mud-nav-link.active:not(.mud-nav-link-disabled) {
    color: var(--mud-palette-primary-lighten);
}

/* ── Account-switcher menu (AivoraAccountButton) ────────────────────────────────
   Give the dropdown a comfortable minimum width so the per-row sign-out icon
   (pushed with ml-auto) pins to the RIGHT edge instead of hugging the email.
   The MudMenu popover is teleported to the popover provider, so this must be a
   GLOBAL rule — component-scoped (CSS-isolation) styles can't reach it. Applied
   via MudMenu's ListClass. */
.aivora-account-menu {
    min-width: 16rem;
}


/* ── File Library upload overlay (AivoraFileLibraryUploadPanel) ──────────────────
   The real <input type="file"> is positioned absolutely over the whole drop zone
   (see InputClass in the component) so the browser's native drag/drop lands on the
   input directly. opacity:0 keeps it invisible YET still interactive — the MudBlazor
   invisible utility (visibility:hidden) would block drag events, so this has no
   utility equivalent and lives here. The drop-surface border/layout uses MudBlazor
   utility classes (border-2 border-dashed …), which are always loaded. */
.aivora-file-upload-overlay {
    opacity: 0;
    cursor: pointer;
}

/* ── Password-change pair state ───────────────────────────────────────────────────
   MudTextField has an error state but no equivalent success state. The pair wrapper
   therefore gives both New and Confirm the same semantic outline: mismatch after one
   or both fields contain unequal values, and success only after an equal pair also
   satisfies the password policy. Helper text says "Passwords match" / "Passwords do
   not match", so the state never depends on colour alone.

   MudBlazor selects three different palette variables for the normal, hover, and focus
   outlines. Scoping those existing variables to each MudInput makes all three states
   agree without overriding MudBlazor declarations (including its !important error
   rule). Use the darker success shade on a light surface for text/non-text contrast
   and the base success shade on the dark surface, matching aivora-strong-success. */
.aivora-password-pair-match .mud-input {
    --mud-palette-lines-inputs: var(--mud-palette-success-darken);
    --mud-palette-action-default: var(--mud-palette-success-darken);
    --mud-palette-primary: var(--mud-palette-success-darken);
}

.aivora-password-pair-match.aivora-password-pair-match .mud-input-label,
.aivora-password-pair-match.aivora-password-pair-match .mud-input-helper-text {
    color: var(--mud-palette-success-darken);
}

:root[data-theme='dark'] .aivora-password-pair-match .mud-input {
    --mud-palette-lines-inputs: var(--mud-palette-success);
    --mud-palette-action-default: var(--mud-palette-success);
    --mud-palette-primary: var(--mud-palette-success);
}

:root[data-theme='dark'] .aivora-password-pair-match.aivora-password-pair-match .mud-input-label,
:root[data-theme='dark'] .aivora-password-pair-match.aivora-password-pair-match .mud-input-helper-text {
    color: var(--mud-palette-success);
}

.aivora-password-pair-mismatch .mud-input {
    --mud-palette-lines-inputs: var(--mud-palette-error);
    --mud-palette-action-default: var(--mud-palette-error);
    --mud-palette-primary: var(--mud-palette-error);
}

.aivora-password-pair-mismatch.aivora-password-pair-mismatch .mud-input-label {
    color: var(--mud-palette-error);
}

/* The ℹ️ / ✏️ affordances of the Translatable family. They are real <button>s, not spans:
   the browser then supplies focusability, Enter/Space activation (with Space NOT scrolling the
   page) and role=button for free — which is why those components carry no tabindex and no
   keydown handler. This class only removes the chrome a button would otherwise draw.
   Guarded by TooltipTriggerReachableGuardTests. */
.aivora-affordance {
    appearance: none;
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    font: inherit;
    line-height: 1;
    color: inherit;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
}

/* One catalog-backed touch minimum for shared controls. The typed CSS default follows
   the existing Styling registration pattern; the catalog/default consistency test guards it.
   Fine-pointer geometry stays with MudBlazor. Top-bar sizing remains with its separate task
   (owner scope decision, 2026-10-12), so no application-bar descendant is adopted here. */
@property --aivora-touch-target-minimum-size {
    syntax: "<length>";
    inherits: true;
    initial-value: 44px;
}

/* The sign-in card's sizes (Styling ▸ Layout, read by AivoraAuthCardFrame): a company sets them in its
   theme like its colours and corners. Typed defaults follow the touch-target pattern above; the
   catalog/default consistency test guards each default here and in the card's own CSS. */
@property --aivora-signin-card-width {
    syntax: "<length>";
    inherits: true;
    initial-value: 450px;
}

@property --aivora-signin-card-padding {
    syntax: "<length>";
    inherits: true;
    initial-value: 36px;
}

@property --aivora-signin-icon-size {
    syntax: "<length>";
    inherits: true;
    initial-value: 56px;
}

@media (any-pointer: coarse) {
    .aivora-touch-target.aivora-touch-target:not(.mud-appbar *),
    .aivora-affordance:not(.mud-appbar *) {
        min-width: var(--aivora-touch-target-minimum-size, 44px);
        min-height: var(--aivora-touch-target-minimum-size, 44px);
    }

    .aivora-affordance:not(.mud-appbar *) {
        justify-content: center;
    }
}

.aivora-affordance:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: 2px;
    border-radius: 2px;
}


/* ─── Surfaces that may never become see-through ──────────────────────────────────────────────
   A tenant makes its page surfaces translucent by lowering the alpha on `--mud-palette-surface`,
   which is a colour it already owns. No new token, no component parameter, no class on any caller —
   measured on the R&D testbed 2026-09-22, where a 35% alpha made every card translucent with the
   background effect reading through and the text still legible.

   ⚠️ The token is emitted as a CSS COLOUR, and which form varies: the same page served
   `rgba(55,55,64,1)` under MudBlazor's defaults and `#1E293B` once a tenant theme had loaded. An
   earlier version of this comment promised rgba always, which was a generalisation from one
   reading. Nothing here depends on the form — `color-mix()` and the two layers below accept any of
   them — but a rule written to PARSE the value would have been broken by the second theme.

   ⚠️ But 31 rules read that one token, and two kinds of thing it feeds must NEVER go translucent:

     * FLOATING surfaces — a dialog, a menu, a select, a picker. These sit ON TOP of page content,
       so a translucent one shows the words behind it THROUGH its own words. Measured on the same
       page: the theme menu became unreadable the moment the cards became pretty.
     * DENSE surfaces — a table and its sticky header. A moving background behind a grid of rows is
       the same defect, just slower to notice.

   ⛔ NOT the snackbar, although it floats. Every snackbar we render is MudBlazor's default FILLED
   variant, painted in its severity colour, which is already opaque — and its text colour is chosen
   for THAT fill (success-text on success). Listing it here painted the surface over the green, so
   "Credits added" became near-black on dark blue: 1.2:1, measured 2026-09-25. Guarded by
   MudBlazorColourContrastGuardTests.FilledSnackbar_KeepsItsOwnFill_SoItsTextStaysReadable.

   Tooltips also retain their own fill/on-colour pair. Although MudBlazor renders them as popovers,
   the generic surface IMAGE must not cover their default dark fill or a selected semantic fill:
   white surface over dark-text hid the entire description in the navigation browser check.
   Exclude the tooltip itself; ordinary menu/select/popover surfaces remain opaque.

   The fix is one rule, and it needs no second colour. Painting the surface colour over the page
   background as two stacked layers flattens whatever alpha the tenant chose: at alpha 1 the result
   is exactly the surface colour it always was, and at any lower alpha it is that colour composited
   onto an opaque base, so the element stays opaque no matter what the theme says.

   ⚠️ `background-image`, not `background-color`, and deliberately: `background-color` is what the
   MudBlazor rules themselves set, so overriding it would be a specificity fight that a future
   MudBlazor release could silently win. A background IMAGE paints above the background colour
   whatever that colour turns out to be, so this rule cannot be outvoted — it can only be removed. */
.mud-popover:not(.mud-tooltip),
.mud-popover:not(.mud-tooltip) .mud-paper,
.mud-dialog,
.mud-table,
.mud-table-sticky-header * .mud-table-root .mud-table-head .mud-table-cell,
.mud-table-sticky-footer * .mud-table-root .mud-table-foot * .mud-table-cell,
.mud-simple-table.mud-table-sticky-header * table thead * th,
.mud-simple-table.mud-table-sticky-footer * table tfoot * td,
.place-search-popover .mud-autocomplete-after-items {
    background-image:
        linear-gradient(var(--mud-palette-surface), var(--mud-palette-surface)),
        linear-gradient(var(--mud-palette-background), var(--mud-palette-background));
}

/* ─── The place-search provider's logo stays in sight ─────────────────────────────────────────
   Google's terms: its logo is "always visible" beside its suggestions (location-data-type D13).
   Five suggestions measured taller than the dropdown (321 px in a 300 px list), so the list
   scrolls, and the logo below the last one scrolled out of sight. Its wrapper is pinned to the
   list's bottom edge — and made an opaque surface in the rule above, because suggestions scroll
   underneath it.

   ⚠️ Global, not in AivoraLocationField.razor.css, and deliberately: the wrapper is MudBlazor's
   element inside a popover rendered OUTSIDE the field's DOM, so a scoped rule — even ::deep —
   cannot reach it. The field names its popover `place-search-popover` to scope this by hand. */
.place-search-popover .mud-autocomplete-after-items {
    position: sticky;
    bottom: 0;
}


/* Each effect reads its own numeric scales. Invalid values use the catalogue default. */
@property --aivora-background-aurora-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-aurora-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-interactive-grid-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-interactive-grid-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-flickering-grid-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-flickering-grid-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-particle-field-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-particle-field-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-drift-field-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-drift-field-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-water-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-water-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-northern-lights-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-northern-lights-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-magnetic-dots-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-magnetic-dots-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-fluid-ink-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-fluid-ink-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-prismatic-lens-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-prismatic-lens-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-ferrofluid-rosette-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-ferrofluid-rosette-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-constellation-network-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-constellation-network-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-elastic-contours-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-elastic-contours-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-silk-ribbons-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-silk-ribbons-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-parallax-landscape-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-parallax-landscape-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-bioluminescent-cells-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-bioluminescent-cells-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-orbital-field-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-orbital-field-speed-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-picture-opacity-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

/* ─── The per-effect settings, REGISTERED for the same reason ─────────────────────────────────
   Each effect's own numbers, one sub-folder per effect in the Styling tree. Every initial value is
   the number the effect hard-coded before the setting existed, so an unusable value lands on the
   effect as it always looked — never on the browser's idea of zero.

   The canvas effects read these from JavaScript too, and registering them helps there as well: the
   browser hands the script the NORMALISED value ("8px", never "8 px" or a word), so the script's own
   parse only has to cover a host that has not loaded this stylesheet. */
@property --aivora-background-interactive-grid-cell-size {
    syntax: "<length>";
    inherits: true;
    initial-value: 44px;
}

@property --aivora-background-interactive-grid-line-width {
    syntax: "<length>";
    inherits: true;
    initial-value: 1px;
}

@property --aivora-background-interactive-grid-glow-radius {
    syntax: "<length>";
    inherits: true;
    initial-value: 150px;
}

@property --aivora-background-flickering-grid-cell-size {
    syntax: "<length>";
    inherits: true;
    initial-value: 8px;
}

@property --aivora-background-flickering-grid-gap {
    syntax: "<length>";
    inherits: true;
    initial-value: 2px;
}

@property --aivora-background-flickering-grid-churn {
    syntax: "<number>";
    inherits: true;
    initial-value: 0.06;
}

@property --aivora-background-fluid-ink-size-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-fluid-ink-pointer-influence {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-prismatic-lens-size-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-prismatic-lens-pointer-influence {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-ferrofluid-rosette-size-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-ferrofluid-rosette-pointer-influence {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-constellation-network-size-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-constellation-network-pointer-influence {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-elastic-contours-size-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-elastic-contours-pointer-influence {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-silk-ribbons-size-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-silk-ribbons-pointer-influence {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-parallax-landscape-size-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-parallax-landscape-pointer-influence {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-bioluminescent-cells-size-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-bioluminescent-cells-pointer-influence {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-orbital-field-size-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-orbital-field-pointer-influence {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-magnetic-dots-size {
    syntax: "<length>";
    inherits: true;
    initial-value: 2px;
}

@property --aivora-background-magnetic-dots-count-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-magnetic-dots-pointer-influence {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-particle-field-size {
    syntax: "<length>";
    inherits: true;
    initial-value: 3px;
}

@property --aivora-background-particle-field-count-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-drift-field-wave-size {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-drift-field-pointer-influence {
    syntax: "<number>";
    inherits: true;
    initial-value: 0.35;
}

@property --aivora-background-water-wave-size {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-water-pointer-influence {
    syntax: "<number>";
    inherits: true;
    initial-value: 0.35;
}

@property --aivora-background-northern-lights-curtain-height {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-background-northern-lights-star-count-scale {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}


/* ─── Focus glow ─────────────────────────────────────────────────────────────────────────────
   A soft halo in the theme primary around the control that has keyboard focus. Tuned by the tenant
   in Styling ▸ Focus glow; every host links this stylesheet, so one rule covers them all.

   ⚠️ Keyboard focus is the browser's :focus-visible, and the browser decides it per control: a TEXT
   FIELD matches after a mouse click too (the caret shows where typing will land), while a button,
   checkbox or chip matches only when reached from the keyboard. This rule does not second-guess that.

   ⚠️ The glow goes on the element that DRAWS the outline, not on the one that takes focus. A MudBlazor
   text field focuses a bare <input>, but its visible border is the sibling fieldset, so the halo is
   placed there through :has() and follows the field's rounded corners. A checkbox or switch focuses a
   hidden <input> inside its round button, which is the element that glows.

   ⚠️ Replaces a filled button's own elevation shadow while it has keyboard focus — box-shadow is one
   property and CSS cannot append to a value it did not write. The glow is itself a shadow, so the
   button still reads as raised.

   Off is the switch, multiplied into the colour: at 0 the halo is fully transparent, and nothing else
   about the control changes. Size and softness are floored at 0 because a negative blur makes the
   whole shadow invalid; the switch is clamped because a bypassed write could set any number. */
@property --aivora-focus-glow-enabled {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-focus-glow-size {
    syntax: "<length>";
    inherits: true;
    initial-value: 3px;
}

@property --aivora-focus-glow-softness {
    syntax: "<length>";
    inherits: true;
    initial-value: 8px;
}

@property --aivora-focus-glow-opacity {
    syntax: "<number>";
    inherits: true;
    initial-value: 0.4;
}

/* The save pulse's settings, registered for the same reason as every token above: an unusable value
   must land on the default, not on the browser's zero. The ring is drawn by `.aivora-save-pulse`
   below. */
@property --aivora-save-pulse-enabled {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-save-pulse-duration {
    syntax: "<time>";
    inherits: true;
    initial-value: 0.6s;
}

@property --aivora-save-pulse-opacity {
    syntax: "<number>";
    inherits: true;
    initial-value: 0.5;
}

/* The save pulse — one soft ring in the theme SUCCESS colour on the button inside `aivora-save-pulse`.
   The save-status chip adds the class when every save in flight has landed; the Styling editor's
   preview (StyleTokenPreview) adds it to a sample. Success, not primary, so a saved chip never looks
   like the focus glow. Shared here, not in the chip's scoped stylesheet, so both draw the SAME ring.

   Plays ONCE: an animation runs when its class is added (or its element is new), and the class then
   stays until the next save starts. The duration is floored at 0.1s because a zero duration would end
   before it began, and the switch is clamped because a bypassed write could set any number. */
.aivora-save-pulse .mud-button-root {
    animation: aivora-save-pulse max(0.1s, var(--aivora-save-pulse-duration, 0.6s)) ease-out 1;
}

@keyframes aivora-save-pulse {
    from {
        box-shadow: 0 0 0 0 color-mix(in srgb,
            var(--mud-palette-success)
            calc(clamp(0, var(--aivora-save-pulse-enabled, 1), 1) * var(--aivora-save-pulse-opacity, 0.5) * 100%),
            transparent);
    }

    to {
        box-shadow: 0 0 0 10px transparent;
    }
}

/* Reduced motion: no ring. The chip's "all saved" label already says it without moving. */
@media (prefers-reduced-motion: reduce) {
    .aivora-save-pulse .mud-button-root {
        animation: none;
    }
}


/* ─── Cursor spotlight ───────────────────────────────────────────────────────────────────────
   The 1px edge of a clickable tile, lit nearest the mouse. A tile opts in with the class
   `aivora-spotlight` (AivoraStatTile adds it only when it has a click handler); js/aivora-spotlight.js
   writes the mouse position onto the element under it. Tuned in Styling ▸ Cursor spotlight.

   The light is a radial gradient masked down to a 1px ring, so only the edge ever glows — the tile's
   content is never tinted. ⚠️ Off unless --aivora-spotlight-on is set, which the script does for the
   mouse only: with no mouse (touch), or before the script runs, the ring is transparent. */
@property --aivora-cursor-spotlight-enabled {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}

@property --aivora-cursor-spotlight-radius {
    syntax: "<length>";
    inherits: true;
    initial-value: 120px;
}

@property --aivora-cursor-spotlight-intensity {
    syntax: "<number>";
    inherits: true;
    initial-value: 0.6;
}

.aivora-spotlight {
    position: relative;
}

.aivora-spotlight::after {
    content: '';
    position: absolute;
    inset: 0;
    padding: 1px;
    border-radius: inherit;
    pointer-events: none;
    background: radial-gradient(
        circle max(1px, var(--aivora-cursor-spotlight-radius, 120px))
            at var(--aivora-spotlight-x, 50%) var(--aivora-spotlight-y, 50%),
        color-mix(in srgb,
            var(--mud-palette-primary)
            calc(clamp(0, var(--aivora-cursor-spotlight-enabled, 1), 1)
                * var(--aivora-cursor-spotlight-intensity, 0.6)
                * var(--aivora-spotlight-on, 0) * 100%),
            transparent),
        transparent 70%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
}

/* ⚠️ The button and chip selectors REPEAT their class, and that is the specificity, not a typo. This
   sheet is linked BEFORE MudBlazor's, and MudBlazor ships `.mud-button-filled:focus-visible` and
   `.mud-fab:focus-visible { box-shadow: … }` at (0,2,0) — the same as `.mud-button-root:focus-visible`
   — so the later sheet won and a focused filled button showed MudBlazor's elevation instead of the glow
   (measured 2026-09-24). The repeated class reaches (0,3,0) and wins without `!important`, which would
   also beat a tenant's own override: the convention in colour-usage.instructions.md §6b. MudBlazor's
   one (0,3,0) rule, `.mud-button-group-filled .mud-button-root:focus-visible`, applies only to a
   `MudButtonGroup` with a Variant, and the app's one button group (AivoraGrid) sets none.

   The text-field rules need no repeat: MudBlazor sets no box-shadow on those elements.

   `aivora-focus-glow` is the Styling editor's preview sample (StyleTokenPreview): the same glow on a
   button that is not focused, so a person tuning it sees it without pressing Tab.

   The checkbox / radio / switch input is NOT a direct child of its round button — a switch nests it
   one span deeper (measured) — so the :has() is a descendant match. */
.mud-input.mud-input-outlined:has(:is(input, textarea):focus-visible) > .mud-input-outlined-border,
.mud-input:not(.mud-input-outlined):has(:is(input, textarea):focus-visible),
.mud-button-root.mud-button-root:focus-visible,
.mud-button-root.mud-button-root:has(input:focus-visible),
.mud-chip.mud-chip:focus-visible,
.mud-button-root.mud-button-root.aivora-focus-glow {
    box-shadow: 0 0
        max(0px, var(--aivora-focus-glow-softness, 8px))
        max(0px, var(--aivora-focus-glow-size, 3px))
        color-mix(in srgb,
            var(--mud-palette-primary)
            calc(clamp(0, var(--aivora-focus-glow-enabled, 1), 1) * var(--aivora-focus-glow-opacity, 0.4) * 100%),
            transparent);
}

/* Admin header geometry is shared. The shared header derives this variable from existing
   host state, so all offsets agree even on supported WebViews without CSS parent selectors.
   These override MudBlazor toolbar/fixed offsets; utilities cannot express that geometry. */
.aivora-admin-layout { --aivora-admin-header-height: 56px; }
.aivora-admin-appbar.mud-appbar .mud-toolbar { height: var(--aivora-admin-header-height); min-height: var(--aivora-admin-header-height); padding-inline: 8px; }
.aivora-admin-header { width: 100%; min-width: 0; }
.aivora-admin-header-row { height: 56px; }
.aivora-admin-title { min-width: 0; }
/* D9: the two title lines share the 56px row; tight line height keeps both inside it. */
.aivora-admin-title .mud-typography { line-height: 1.3; }
.aivora-admin-context { font-weight: 600; }
.aivora-admin-header-row > .mud-menu,
.aivora-admin-header-row > .aivora-save-chip { flex-shrink: 0; }
.aivora-admin-status { height: 44px; }
.aivora-admin-status .mud-icon-button { width: 44px; height: 44px; padding: 10px; }
.aivora-admin-status .mud-typography { min-width: 0; }
.aivora-admin-header-row .aivora-save-chip .mud-button-root { min-height: 44px; }
.aivora-account-trigger.mud-icon-button { width: 48px; height: 48px; padding: 8px; }
.aivora-admin-layout .mud-main-content { padding-top: var(--aivora-admin-header-height); }
.aivora-admin-layout.aivora-admin-layout .mud-drawer-fixed.mud-drawer-clipped-always { top: var(--aivora-admin-header-height); height: calc(100% - var(--aivora-admin-header-height)); }

/* Empty inherited dots reuse the host foreground, like their annotated icon.
   Specificity beats MudBadge default colours regardless of stylesheet load order. */
.aivora-dot-badge-inherit .mud-badge.mud-badge-dot {
    color: inherit;
    background-color: currentColor;
}
