/* 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 {
    flex: 1 1 auto;
    min-height: 0;
}

.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 seven 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 file tree'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.
   MudBlazor zeroes the padding of every icon button inside a dense tree
   (.mud-treeview-dense .mud-icon-button{padding:0}), which would leave an 18px tap target on the one
   control that exists for fingers. Equal specificity (0,2,0), and this file loads after MudBlazor.min.css,
   so it wins; rows grow a few px, which a phone can afford and a mouse never notices. */
.browse-row-menu .mud-icon-button {
    padding: 6px;
}

/* A file tree 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.
   The child combinator is exact and not a style choice: MudTreeViewItem's Class lands on the <li>, whose
   direct children are this row's content div and (for a folder) the group <ul>, so a descendant selector
   would tint a whole subtree the day a folder gets a marker. Files are leaves today, so nothing bleeds
   either way — the `>` is what keeps that true later.
   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.
   Selecting the row restores the primary colour — MudBlazor's .mud-treeview-selected-primary rule is
   (0,3,0) against this (0,2,0). That is deliberate: selection feedback outranks a status hint.
   Colour is never the only signal — the label says "not connected" and the row carries a title. */
.browse-unavailable > .mud-treeview-item-content {
    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. */
.preview-dialog.mud-dialog-fullscreen .mud-dialog-content {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
}

.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. 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;
}

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

.kdbx-layout {
    height: 70vh;
}

.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;
    }

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

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

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

    .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;
}
