/* Shared responsive behavior for the Blazor server UI. */

/* The app bar's height at the current breakpoint. MudBlazor sizes both .mud-appbar's toolbar and
   .mud-main-content's padding-top on this three-step scale (same media conditions, reproduced here in
   the same order), so anything that measures "the space left below the app bar" has to follow it too.
   Also: MudContainer's mt-6 above every page, mirrored as breathing room below. */
:root {
    --filesync-appbar-height: calc(var(--mud-appbar-height) - var(--mud-appbar-height) / 8);
    --filesync-content-gap: 24px;
}

@media (orientation: landscape) {
    :root {
        --filesync-appbar-height: calc(var(--mud-appbar-height) - var(--mud-appbar-height) / 4);
    }
}

@media (min-width: 600px) {
    :root {
        --filesync-appbar-height: var(--mud-appbar-height);
    }
}

/* MudBlazor gives this a flat var(--mud-appbar-height), which leaves it 8px taller than the app bar
   below the sm breakpoint. This file loads after MudBlazor.min.css at equal specificity, so it wins. */
.mud-drawer-header {
    min-height: var(--filesync-appbar-height);
}

/* Fill the viewport rather than growing past it: the scroll region below takes the leftover height, so
   a long list scrolls inside its panel and the document itself never gains a second scrollbar. */
.fill-viewport {
    display: flex;
    flex-direction: column;
    height: calc(100vh - var(--filesync-appbar-height) - var(--filesync-content-gap) * 2);
    height: calc(100dvh - var(--filesync-appbar-height) - var(--filesync-content-gap) * 2);
    min-height: 320px;   /* on a very short viewport let the page scroll rather than crush the panel */
}

/* Holds the leftover space whichever of loading / empty / results is showing, so the layout doesn't
   jump as the content changes. */
.fill-viewport-body {
    display: flex;
    flex-direction: column;
}

.fill-viewport-scroll {
    overflow: auto;
}

/* MudBlazor components bring their own flex — a MudTextField's .mud-input-control is `flex: 1 1 auto`
   — so an unpinned child grows to swallow the column's free space and shoves the scroll region to the
   bottom. Reset every child to the browser default (0 1 auto: don't grow, may still shrink on a
   cramped viewport), then hand growth to the two elements that should have it. */
.fill-viewport > *,
.fill-viewport-body > * {
    flex: 0 1 auto;
}

/* min-height:0 is load-bearing on both — a flex item won't shrink below its content without it, and
   the outer scrollbar comes straight back. Same trap as the MudDataGrid one noted in CLAUDE.md.
   (0,2,0) against the pin's (0,1,0), so these win regardless of order. */
.fill-viewport > .fill-viewport-body,
.fill-viewport-body > .fill-viewport-scroll,
.fill-viewport-body > .fill-viewport-grid {
    flex: 1 1 auto;
    min-height: 0;
}

/* The file browser's listing. MudDataGrid's Height lands on .mud-table-container, and its height:100%
   only resolves because the grid's own root is the flex child above — .mud-table declares no height of
   its own. That is why the grid sits there directly with no MudPaper around it: a clipping wrapper makes
   the container grow to the full content height and strands every row below the fold with no scrollbar,
   and a scrolling one gives two. Same trap KdbxPreviewPanel documents — and Virtualize *requires* that
   Height, so getting it wrong here breaks the listing rather than merely looking odd.

   Every row must be exactly the ItemSize the component passes (RowHeightPx), because Virtualize sizes
   its spacers from that one number: a row that wraps makes the scrollbar a lie and the scroll position
   drift. Hence nowrap and ellipsis on every cell, with the untruncated text on the row's title — which
   is the visible cost of virtualizing, and why .wrap-anywhere is deliberately absent here. */
/* The height has to be pinned on the *cell*, not the row: in table layout a tr's height is only a
   minimum, and the row ends up as tall as its tallest cell's content plus padding. MudBlazor's dense
   padding is 6px top and bottom, which measured 69px rows against the 32 this claims — a virtualized
   scrollbar sized from the smaller number would have been wrong by more than double. Zeroing the block
   padding and fixing the line height is what makes the measured row equal RowHeightPx.

   max-width:0 is the ellipsis idiom for a table cell: with no width to shrink against, a cell grows to
   fit its content and text-overflow never applies. */
.browse-grid .mud-table-row .mud-table-cell {
    height: 32px;
    line-height: 20px;
    max-width: 0;
    overflow: hidden;
    padding-bottom: 0;
    padding-top: 0;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.browse-grid .browse-col-size,
.browse-grid .browse-col-modified {
    width: 15%;
}

.browse-grid .browse-col-actions {
    padding-left: 0;
    padding-right: 4px;
    width: 48px;
}

/* The name cell's parts on one line: an icon that never shrinks, then a label that takes the rest and
   clips. The cell already clips, but the flex item needs min-width:0 of its own or it refuses to shrink
   below its content — the same rule the dialog height chain turns on. */
.browse-name {
    align-items: center;
    display: flex;
    gap: 8px;
    min-width: 0;
    overflow: hidden;
}

.browse-name-icon {
    flex: none;
}

.browse-name-text,
.browse-open {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Opening a folder and selecting it are different acts with different targets, so the name has to look
   like the one that opens. Not an <a>: there is no URL behind it, and a real anchor would offer a
   context menu and a middle-click that lead nowhere. */
.browse-open {
    cursor: pointer;
    text-decoration: underline;
    text-decoration-style: dotted;
    text-underline-offset: 2px;
}

.browse-open:hover,
.browse-open:focus-visible {
    text-decoration-style: solid;
}

/* The row a click selected. MudDataGrid draws no selection of its own without a SelectedItem binding,
   and the toolbar acts on _selectedPath, so this is the only thing that says which row that is. */
.browse-grid .mud-table-row.browse-selected {
    background-color: var(--mud-palette-action-default-hover);
}

.wrap-anywhere,
.wrap-anywhere .mud-typography,
.responsive-table .mud-table-cell,
.responsive-grid .mud-table-cell {
    overflow-wrap: anywhere;
    word-break: break-word;
}

.responsive-actions {
    flex-wrap: wrap;
}

/* The file browser's two bars carry more controls than any other toolbar here — up to eleven in the
   action bar alone — so they take this instead of .responsive-actions, whose mobile rule below gives
   every child a full-width row of its own. That is right for a two-button toolbar and wrong here: it
   spent more vertical space on chrome than was left for the tree underneath, on the one screen where
   there is least to spare. A separate class rather than a modifier, because beating that rule's
   !important column would take another !important. */
.compact-actions {
    flex-wrap: wrap;
}

/* The listing's per-row ⋮ menu button. Deliberately not inside the phone breakpoint: what is broken is
   *touch*, not *narrow* — iOS Safari never fires `contextmenu` on a long press, and an iPad reports 810px
   in portrait, so a max-width rule would leave the very device that needs this without it. It is the one
   control in a row that exists for fingers, and the grid is Dense, so it gets a real target of its own.
   The class now lands on the button itself rather than on a MudMenu wrapping one, hence no descendant
   combinator. Keep the padding inside the row height .browse-grid pins, or every row grows and
   Virtualize's ItemSize stops matching what it measures. */
.browse-row-menu.mud-icon-button {
    padding: 4px;
}

/* A listing row no client with a live hub connection holds — nothing can preview, stream or prepare a
   download from it right now, which is the same question ClientFileFetchService answers on the click.
   RowClassFunc puts this on the grid's own <tr>, so it tints that row and nothing else; it used to hang
   off MudTreeViewItem's content div, where a descendant selector would have tinted a whole subtree.
   The leading icon dims with the text only because the row passes IconColor="Color.Inherit": MudIcon
   emits *no* colour class for Inherit, leaving .mud-icon-root.mud-svg-icon{fill:currentColor} to pick
   this up. Color.Default would pin it to --mud-palette-text-secondary and it would stay bright.
   Colour is never the only signal — the label says "not connected" and the row carries a title. */
.browse-grid .mud-table-row.browse-unavailable {
    color: var(--mud-palette-text-disabled);
}

.responsive-dialog .mud-dialog-content {
    min-width: 0;
}

.responsive-dialog .mud-dialog-actions {
    flex-wrap: wrap;
}

/* A text preview is the one preview kind that deliberately takes over the viewport. MudDialog's content
   has to participate in the height chain or a multiline field sizes itself from Lines and leaves most of
   the screen unused. The field is read-only, so native resize handles would only fight this layout.

   Applied to every preview frame, not only the full-screen one, and that is Previous/Next's doing:
   MudDialog captures its Options when it opens and an inline dialog never sees a later change, so the
   FullScreen chosen for the file the dialog was *opened* with is the frame for the whole stepping
   session. Stepping from an image to a log therefore lands a text preview in a Large dialog, where the
   fullscreen-only rule left it collapsed to its Lines default. Making the chain unconditional lets the
   panel fill whichever frame it finds itself in; max-height keeps the non-fullscreen case from growing
   past the viewport, which .mud-dialog-fullscreen handles for itself.

   flex-direction:column is load-bearing, and leaving it out is what made that lift a regression. `display:
   flex` defaults to *row*, so a panel sized only by a height shrink-to-fits horizontally: .docx-host and
   .kdbx-layout collapsed to their content's intrinsic width — 300px for the docx host, which is an
   iframe's default — and docx-preview centres its page inside that, putting the left half of every line
   out of scroll reach. .text-preview-panel survived only because it happens to carry width:100%. Column
   also restores cross-axis stretch, which is where every panel's width comes from, and stacks
   DocxPreviewPanel's progress bar and error alert above the document rather than beside it.

   box-sizing:content-box is what stops the cap eating the panels' last 16px. MudBlazor gives
   .mud-dialog-content padding:8px 24px under border-box, so a max-height:70vh box holds only 70vh-16px of
   content and overflow:hidden hides the difference with no scrollbar. Capping the *content area* instead
   needs no knowledge of what that padding is.

   The 70vh stays on the panels rather than moving here, deliberately: a definite height on this box would
   give the audio player, the "no inline preview" alert and a locked vault's password form a 70vh dialog,
   where each sizes to its own content today. .text-preview-panel is the exception, because it is the one
   panel with no intrinsic height worth filling from — which is the stepping case above. */
.preview-dialog .mud-dialog-content {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    min-height: 0;
    overflow: hidden;
}

.preview-dialog:not(.mud-dialog-fullscreen) .mud-dialog-content {
    box-sizing: content-box;
    max-height: 70vh;
}

.preview-dialog:not(.mud-dialog-fullscreen) .text-preview-panel {
    height: 70vh;
}

/* The preview dialog's Previous/Next group. margin-right:auto puts it at the leading edge and leaves
   Download and Close where every other dialog here keeps them; .responsive-dialog already sets flex-wrap
   on .mud-dialog-actions, so under 600px it takes its own row rather than squeezing the two buttons that
   matter more. These are ordinary MudIconButtons in a dialog, so the .browse-row-menu tap-target problem
   does not apply — nothing zeroes their padding and the default target is already finger-sized. */
.preview-stepper {
    align-items: center;
    display: flex;
    gap: 4px;
    margin-right: auto;
}

/* Focused programmatically on every open, so a plain :focus ring would flash on a mouse-driven preview
   that never touches the keyboard. :focus-visible is exactly the "being driven by keyboard" signal, and
   it must keep a ring: this element is the arrow keys' only landing site. */
.preview-stepper:focus {
    outline: none;
}

.preview-stepper:focus-visible {
    border-radius: 4px;
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: 2px;
}

.preview-position {
    font-size: 0.875rem;
    white-space: nowrap;
}

/* The preview *page* — the same preview the dialog shows, on a route of its own so a file can be
   bookmarked. The dialog's height chain hangs off .mud-dialog-content, which does not exist here, so
   the same three facts are restated on an element of ours; everything the note on .preview-dialog above
   says applies unchanged, flex-direction:column included and for the same shrink-to-fit reason.

   It is spelled *alongside* .fill-viewport-body rather than instead of it, and that is load-bearing:
   `.fill-viewport > * { flex: 0 1 auto }` is a class plus the universal selector, so a class of our own
   ties with it on specificity and the winner is decided by source order — the panel would silently
   collapse to its intrinsic height. .fill-viewport > .fill-viewport-body already beats the pin, so this
   class only has to add what is left.

   box-sizing:content-box is deliberately NOT copied from the dialog. It exists there to stop MudBlazor's
   own .mud-dialog-content padding eating the last 16px of a capped box; there is no such padding here,
   and content-box on a flex container with its own padding is a different bug. */
.preview-page-body {
    overflow: hidden;
}

/* On the page the body *is* the frame, so the panels that fill take the flex chain instead of the
   fraction-of-viewport heights they use inside a dialog. Scoped under .preview-page-body, which never
   exists inside one, so the .preview-dialog rules above and the phone overrides below are untouched. */
.preview-page-body .text-preview-panel,
.preview-page-body .docx-host,
.preview-page-body .kdbx-layout {
    flex: 1 1 auto;
    height: auto;
    min-height: 0;
}

/* The action bar, mirroring .mud-dialog-actions so .preview-stepper's margin-right:auto and the Download
   button's ms-auto mean the same thing here as they do there. Wraps under 600px for the same reason
   .responsive-dialog does: seven controls do not fit on a phone, and the stepper is the one that can
   afford its own row. */
.preview-page-actions {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    padding-top: 8px;
}

.text-preview-panel {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    min-height: 0;
    width: 100%;
}

.text-preview-field {
    flex: 1 1 auto;
    min-height: 0;
}

.text-preview-field .mud-input-control-input-container,
.text-preview-field .mud-input,
.text-preview-field textarea {
    height: 100%;
    min-height: 0;
}

.text-preview-field textarea {
    box-sizing: border-box;
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
    height: 100% !important;
    margin-bottom: 0;
    margin-top: 0;
    overflow: auto !important;
    overflow-wrap: normal;
    resize: none;
    white-space: pre;
}

/* The home page's Files card lists one deep link per accessible share, and an Admin sees every share on
   the server. MudGrid stretches each item to the tallest card in the row — which is exactly what the
   cards' height:100% relies on to keep their action buttons on one line — so an unbounded list does not
   just make this card tall, it drags the whole row's "Open" buttons down past the fold. Cap it and scroll
   instead: ~7 rows at the links' py-1 metrics. A fixed max-height on a plain block child of
   .mud-card-content, so none of the min-height:0 flex machinery elsewhere in this file applies.
   overscroll-behavior keeps a flick at the end of the list from scrolling the page underneath. */
.home-share-list {
    max-height: 200px;
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* ---------------------------------------------------------------------------------------------------
   The public landing page (Components/Pages/PublicLanding.razor) and its black-hole mark
   (Components/BlackHoleLogo.razor). Everything visual for those two lives here, so the markup carries
   structure only — which is also what lets the mark take its colours from the theme: stop-color is a
   CSS-settable presentation attribute, so an inline SVG's gradients can read --mud-palette-* like the
   rest of the app instead of hard-coding hexes Themes.cs would then own a second copy of.
   --------------------------------------------------------------------------------------------------- */

.landing-hero {
    align-items: center;
    display: flex;
    flex-direction: column;

    /* Clips ::before below, whose inset reaches 20% of the hero's width past each side. That bleed is the
       effect and is worth keeping — but it is proportional, so left unclipped it puts a horizontal
       scrollbar on the page at every viewport width, not just narrow ones. Clipping at the hero is what
       makes the glow stop at the edge of the screen instead. */
    overflow: hidden;
    padding: 24px 0 48px;
    position: relative;
    text-align: center;
}

/* The hole's light bleeding into the page behind the mark. Sized in % of the hero so it tracks the
   viewport, and pushed behind everything with a z-index the flow content does not have to opt out of
   (the children below get position:relative rather than a z-index each). The plain background line is
   the fallback for a browser without color-mix; there it simply gets no glow, which is fine. */
.landing-hero::before {
    background: transparent;
    background: radial-gradient(58% 46% at 50% 30%,
        color-mix(in srgb, var(--mud-palette-primary) 20%, transparent),
        color-mix(in srgb, var(--mud-palette-tertiary) 12%, transparent) 45%,
        transparent 72%);
    content: "";
    inset: -10% -20% 20%;
    pointer-events: none;
    position: absolute;
    z-index: 0;
}

.landing-hero > * {
    position: relative;
    z-index: 1;
}

.landing-logo {
    display: block;
    margin-bottom: 8px;
}

/* Gradient wordmark. The solid colour first is not decoration: -webkit-text-fill-color:transparent
   would leave the word invisible in anything that ignores background-clip, so the fallback has to be a
   real declaration that such a browser keeps. */
.landing-title {
    color: var(--mud-palette-text-primary);
    background: linear-gradient(100deg, var(--mud-palette-secondary), var(--mud-palette-primary) 45%,
        var(--mud-palette-tertiary));
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    font-weight: 600;
    letter-spacing: -0.02em;
    margin-bottom: 8px;
}

.landing-tagline {
    color: var(--mud-palette-text-primary);
    font-weight: 400;
    margin-bottom: 16px;
    max-width: 34ch;
}

.landing-lede,
.landing-download > .mud-typography-body1 {
    margin-left: auto;
    margin-right: auto;
    max-width: 62ch;
}

.landing-cta {
    margin-top: 32px;
}

.landing-features {
    margin-bottom: 24px;
}

/* Elevation="0" on the MudPaper, so the tile is drawn by its border rather than by a shadow — the hero
   above it is already the loud element and two competing depths read as clutter. */
.landing-feature {
    background-color: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-divider);
    height: 100%;
    transition: border-color 150ms ease, transform 150ms ease;
}

.landing-feature:hover {
    border-color: var(--mud-palette-primary);
    transform: translateY(-2px);
}

.landing-download {
    padding-top: 32px;
    text-align: center;
    scroll-margin-top: calc(var(--filesync-appbar-height) + 16px);   /* the app bar is fixed */
}

/* --- the mark ------------------------------------------------------------------------------------ */

.black-hole {
    display: block;
}

/* The bloom's colours. flood-color/flood-opacity are CSS properties, so the feDropShadow primitives in
   BlackHoleLogo.razor read the palette like everything else. (A CSS filter: drop-shadow() was the obvious
   way to do this and is wrong here — Firefox casts it from the SVG group's box rather than its alpha,
   which paints a lighter rectangle the size of the mark.) */
.bh-flood-near { flood-color: var(--mud-palette-primary); flood-opacity: 0.5; }
.bh-flood-far { flood-color: var(--mud-palette-tertiary); flood-opacity: 0.35; }

/* Gradient stops, the reason the mark is inline SVG at all. */
.bh-stop-a { stop-color: var(--mud-palette-secondary); }
.bh-stop-b { stop-color: var(--mud-palette-primary); }
.bh-stop-c { stop-color: var(--mud-palette-tertiary); }

/* Flat #000, deliberately not a palette variable: the event horizon is an absence rather than a colour,
   and it has to stay the darkest thing on screen whatever the surface behind it becomes. */
.bh-core { fill: #000; }

.bh-photon {
    stroke: var(--mud-palette-white);
    animation: bh-pulse 6s ease-in-out infinite;
}

.bh-particle { fill: var(--mud-palette-white); opacity: 0.9; }
.bh-particle-far { opacity: 0.65; }

.bh-track { opacity: 0.3; }
.bh-dust { opacity: 0.22; }
.bh-halo { opacity: 0.22; }
.bh-disc-far { opacity: 0.7; }
.bh-disc-near { opacity: 0.95; }

/* transform-box: view-box makes 50% 50% resolve against the 200x200 viewBox — the hole's centre — for
   every element regardless of its own bounding box, which is what lets the dust group carry its two
   particles without them orbiting some point of their own. Differential rotation, inner ring fastest:
   that is what a real accretion disc does, and it reads better than one rigid spin. The accretion ring
   is deliberately the slowest of the three that move — fast enough to be alive, slow enough not to
   become a loading spinner. */
.bh-orbit,
.bh-accretion,
.bh-track {
    transform-box: view-box;
    transform-origin: 50% 50%;
}

.bh-orbit { animation: bh-spin 13s linear infinite; }
.bh-accretion { animation: bh-spin 24s linear infinite; }
.bh-track { animation: bh-spin 62s linear infinite; }

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

@keyframes bh-pulse {
    0%, 100% { opacity: 0.6; }
    50% { opacity: 0.95; }
}

/* Motion here is pure decoration, so it is the whole of what a reduce-motion preference switches off.
   The mark loses nothing legible — it becomes a still black hole rather than a turning one. */
@media (prefers-reduced-motion: reduce) {
    .bh-orbit,
    .bh-accretion,
    .bh-track,
    .bh-photon,
    .landing-feature {
        animation: none;
        transition: none;
    }
}

/* Host for the client-side .docx render, which JS fills with a single sandboxed iframe. The other preview
   kinds pin their element to 70vh; this one has to do it here, or the frame would collapse to the iframe
   default (150px) instead of filling the dialog. width:100% answers that default's other half — an
   iframe with nothing else to say is 300px wide — which bites the moment this div is a flex item rather
   than a block; see the flex-direction note on .preview-dialog above. Scrolling happens *inside* the
   frame, so the host itself does not scroll. docx-preview draws its own page background, so the rendered
   document reads as a white page in either theme — the same as a PDF in the iframe next to it. */
.docx-host {
    height: 70vh;
    width: 100%;
}

.docx-frame {
    border: 0;
    height: 100%;
    width: 100%;
}

/* width:100% for .docx-host's reason: sized only by a height, this would shrink-to-fit as a flex item. */
.kdbx-layout {
    height: 70vh;
    width: 100%;
}

.kdbx-tree {
    width: 280px;
    min-width: 220px;
    overflow: auto;
}

.kdbx-search {
    max-width: 360px;
}

@media (max-width: 600px) {
    .appbar-user {
        display: none;
    }

    /* Keep --filesync-content-gap in step with the smaller margin below, or anything sized off it
       (.fill-viewport) is 8px out on a phone. */
    :root {
        --filesync-content-gap: 16px;
    }

    .filesync-content {
        margin-top: 16px !important;
        padding-left: 12px !important;
        padding-right: 12px !important;
    }

    /* Step page titles down to h5 metrics on a phone, using MudBlazor's own typography scale so they
       stay in step with the theme rather than drifting from it. */
    .filesync-content .mud-typography-h4 {
        font-size: var(--mud-typography-h5-size);
        line-height: var(--mud-typography-h5-lineheight);
    }

    .responsive-actions {
        align-items: stretch !important;
        flex-direction: column !important;
        width: 100%;
    }

    .responsive-actions > *,
    .responsive-actions .mud-button-root,
    .mobile-full-width,
    .mobile-full-width .mud-button-root,
    .mobile-full-width .mud-input-control {
        width: 100%;
        max-width: none !important;
    }

    /* Icon-only, all on one wrapping row. The label is wrapped in a real span in the markup because a
       bare text node cannot be targeted by a selector; hiding it leaves MudButton's start icon as the
       whole of the button's content. The icon is still the same one the desktop button shows, so the
       two widths teach the same vocabulary. */
    .compact-actions .action-label {
        display: none;
    }

    /* Keyed to .action-button rather than .mud-button-root: the search bar carries the Name/Latest
       MudToggleGroup, whose items are buttons too, and squeezing those is not the intent. */
    .compact-actions .action-button {
        min-width: 0;
        padding-left: 10px;
        padding-right: 10px;
        /* 10 + 20 (icon) + 10 leaves a 40px-wide button, so pin the height to match rather than let it
           fall out of the line-height — a phone is exactly where an undersized tap target is felt. */
        min-height: 40px;
    }

    /* MudButton spaces a start icon 8px from its label and pulls it -4px left so it sits optically
       centred against text. With the label gone both are wrong, and the glyph sits visibly off-centre
       in its own button. */
    .compact-actions .action-button .mud-button-icon-start {
        margin-left: 0;
        margin-right: 0;
        margin-inline-start: 0;
        margin-inline-end: 0;
    }

    /* The search field is the one control that should still take the leftover width, so it shares a row
       with the sort toggle rather than claiming one of its own. min-width:0 is what lets an input
       shrink below its intrinsic content width — without it the toggle is pushed onto the next line. */
    .compact-actions .compact-grow {
        flex: 1 1 auto;
        min-width: 0;
    }

    .responsive-table .mud-table-row,
    .responsive-grid .mud-table-row {
        padding-top: 8px;
        padding-bottom: 8px;
    }

    .responsive-table .mud-table-cell[data-label],
    .responsive-grid .mud-table-cell[data-label] {
        align-items: flex-start;
        min-width: 0 !important;
        text-align: left !important;
    }

    .responsive-table .mud-table-cell[data-label]::before,
    .responsive-grid .mud-table-cell[data-label]::before {
        flex: 0 0 42%;
        max-width: 42%;
        padding-right: 12px;
    }

    .responsive-table .mud-table-cell[data-label] > *,
    .responsive-grid .mud-table-cell[data-label] > * {
        min-width: 0;
    }

    .responsive-table .mud-table-cell .mud-stack,
    .responsive-grid .mud-table-cell .mud-stack {
        flex-wrap: wrap;
        min-width: 0;
    }

    /* FullScreen already supplies exact viewport geometry. Keep the comfortable inset only on ordinary
       responsive dialogs, otherwise this rule would quietly turn a phone's text preview back into a card. */
    .responsive-dialog:not(.mud-dialog-fullscreen) {
        height: calc(100dvh - 16px);
        margin: 8px;
        max-height: calc(100dvh - 16px);
        max-width: calc(100vw - 16px) !important;
        width: calc(100vw - 16px);
    }

    .responsive-dialog .mud-dialog-content {
        overflow-x: hidden;
        padding-left: 16px;
        padding-right: 16px;
    }

    /* .responsive-dialog above already sizes the dialog to the viewport, so the 70vh cap would only clip
       inside it — .kdbx-layout's phone height is calc(100dvh - 210px), which is more than 70vh. */
    .preview-dialog:not(.mud-dialog-fullscreen) .mud-dialog-content {
        max-height: none;
    }

    .mobile-card-action .mud-button-root {
        width: 100%;
    }

    .no-access-page {
        margin-top: 32px !important;
    }

    .no-access-page .mud-paper {
        padding: 24px 16px !important;
    }

    /* The listing stays a table — MudDataGrid's own Breakpoint would stack it into cards of varying
       height, which is exactly what Virtualize's single ItemSize cannot survive. Dropping the two
       columns that do not fit leaves name and ⋮, which is all the tree ever showed on a phone. */
    .browse-grid .browse-col-size,
    .browse-grid .browse-col-modified {
        display: none;
    }

    .kdbx-layout {
        flex-direction: column !important;
        height: calc(100dvh - 210px);
        min-height: 420px;
    }

    .kdbx-tree {
        flex: 0 0 auto;
        max-height: 25vh;
        min-height: 100px;
        min-width: 0;
        width: 100%;
    }

    .kdbx-search {
        max-width: none;
        width: 100%;
    }

    .kdbx-footer {
        align-items: flex-start !important;
        flex-wrap: wrap;
    }

    .kdbx-footer .mud-spacer {
        display: none;
    }

    /* The hero titles itself with h2 and h5; the rule above only steps h4 down, so without these the
       wordmark alone is wider than a phone. */
    .filesync-content .landing-title {
        font-size: var(--mud-typography-h4-size);
        line-height: var(--mud-typography-h4-lineheight);
    }

    .filesync-content .landing-tagline {
        font-size: var(--mud-typography-h6-size);
        line-height: var(--mud-typography-h6-lineheight);
    }

    .landing-hero {
        padding: 8px 0 32px;
    }

    .landing-cta {
        margin-top: 24px;
    }
}

/* ---------------------------------------------------------------------------------------------------
   The loading splash (Components/Hosts/AppHost.razor) and the public host (Hosts/PublicHost.razor).

   Both render before — or entirely without — the WebAssembly runtime, so neither has MudBlazor's
   component styles. They do have --mud-palette-*, because each host emits Themes.SplashVariablesCss,
   which is generated from Themes.AppTheme rather than hand-copied. The .bh-* and .landing-* rules above
   therefore work unchanged, which is the whole reason the splash can reuse the real mark.
   --------------------------------------------------------------------------------------------------- */

html, body {
    background: var(--mud-palette-background);
    color: var(--mud-palette-text-primary);
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    margin: 0;
}

.splash {
    justify-content: center;
    min-height: 100vh;
    min-height: 100dvh;   /* excludes mobile browser chrome, which 100vh does not */
}

/* Not .landing-title: that one is a MudBlazor typography class the splash does not have. Same gradient
   wordmark, sized here rather than inherited. */
.splash-title {
    background: linear-gradient(100deg, var(--mud-palette-secondary), var(--mud-palette-primary) 45%,
        var(--mud-palette-tertiary));
    -webkit-background-clip: text;
    background-clip: text;
    color: var(--mud-palette-text-primary);   /* fallback first: -webkit-text-fill-color would hide it */
    -webkit-text-fill-color: transparent;
    font-size: clamp(2rem, 7vw, 3.5rem);
    font-weight: 600;
    letter-spacing: -0.02em;
    margin-bottom: 8px;
}

.splash-status {
    color: var(--mud-palette-text-secondary);
    font-size: 0.95rem;
}

/* Blazor sets --blazor-load-percentage and --blazor-load-percentage-text on the document root while the
   runtime downloads. Printing the number costs nothing and turns "Loading" into progress. */
.splash-percent::after {
    content: " " var(--blazor-load-percentage-text, "");
}

/* The accretion ring IS the progress bar - which is what the mark was drawn to be (see BlackHoleLogo).
   Its own spin is switched off here because a ring that both rotates and fills reads as neither.

   The arithmetic: the ring is r=45, so its circumference is 2*pi*45 = 282.7 user units. A percentage in
   stroke-dasharray resolves against the viewport diagonal, which for the square 200x200 viewBox is 200 -
   so 282.7 units is 141.4%, and one percent of progress is 1.414% of that. The huge second value keeps
   the remainder of the ring blank rather than repeating the dash. */
.splash-mark .bh-accretion {
    animation: none;
    stroke-dasharray: calc(var(--blazor-load-percentage, 0%) * 1.414) 1000%;
    transition: stroke-dasharray 200ms linear;
}

/* Motion is decoration here as it is on the landing page, so a reduce-motion preference switches off the
   same things. The progress ring keeps filling: it is information, not decoration. */
@media (prefers-reduced-motion: reduce) {
    .splash-mark .bh-accretion { transition: none; }
}

/* The framework's unhandled-error bar, which ships unstyled. There is no reconnect overlay any more -
   that belonged to the circuit - so this is the only thing of its kind left. */
#blazor-error-ui {
    background: var(--mud-palette-surface);
    border-top: 1px solid var(--mud-palette-primary);
    bottom: 0;
    box-shadow: 0 -1px 12px rgba(0, 0, 0, 0.4);
    color: var(--mud-palette-text-primary);
    display: none;
    left: 0;
    padding: 12px 16px;
    position: fixed;
    right: 0;
    z-index: 1200;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    float: right;
}

/* --- the public host ------------------------------------------------------------------------------ */

/* A minimal bar instead of MainLayout's app bar and drawer. The drawer needs a toggle, and a toggle needs
   interactivity this document deliberately does not load - a hamburger that did nothing would be worse
   than none. Both links are plain navigations. */
.public-bar {
    align-items: center;
    background: var(--mud-palette-surface);
    display: flex;
    gap: 16px;
    justify-content: space-between;
    padding: 12px 24px;
}

.public-brand {
    color: var(--mud-palette-text-primary);
    font-size: 1.25rem;
    font-weight: 600;
    text-decoration: none;
}

.public-links {
    display: flex;
    gap: 20px;
}

.public-links a {
    color: var(--mud-palette-text-secondary);
    text-decoration: none;
}

.public-links a:hover { color: var(--mud-palette-primary); }

.public-content {
    margin: 0 auto;
    max-width: 1280px;
    padding: 24px;
}

/* The splash's failure state. Reached from AppHost's Blazor.start().catch — a runtime that cannot be
   downloaded otherwise leaves the mark turning forever, which reads as a hang rather than an error. */
.splash-failed {
    color: var(--mud-palette-text-primary);
    max-width: 46ch;
    text-align: center;
}

.splash-failed a {
    color: var(--mud-palette-primary);
    display: inline-block;
    margin-top: 12px;
}
