/* =============================================================================
   Application shell
   -----------------------------------------------------------------------------
   Layout for the chrome that surrounds every page, plus the two pieces of UI
   Blazor's own bootstrap requires (the WebAssembly loading indicator and the
   unhandled-error bar). Everything else is component-scoped CSS.

   Token-only: no literal colours, sizes, radii or shadows.

   RESPONSIVE MODEL
   Two layouts, one breakpoint - 64rem, the --bp-lg of the scale documented in
   tokens.css:

     >= 64rem   sidebar is a permanent column; content is centred and capped
     <  64rem   sidebar becomes a modal drawer opened from the top bar

   A permanent sidebar is worth 16rem of a desktop window and unaffordable on a
   phone, and a nav that is always visible but always cramped serves neither. The
   drawer is genuinely modal below the breakpoint: it sits above the top bar, dims
   what it covers, and closes on Escape, on the overlay, and on navigation.
   ============================================================================= */

/* Anchors the skip link and lets the drawer overlay cover the whole viewport. */
.app-shell {
    display: flex;
    /* dvh accounts for a phone browser's collapsing address bar; vh does not, and
       leaves a strip of background below the fold. vh first as the fallback. */
    min-height: 100vh;
    min-height: 100dvh;
}

/* -----------------------------------------------------------------------------
   Skip link - the first thing a keyboard or screen-reader user meets
   ----------------------------------------------------------------------------- */

.skip-link {
    position: absolute;
    top: var(--space-2);
    left: var(--space-2);
    z-index: var(--z-toast);
    padding: var(--space-2) var(--space-4);
    border-radius: var(--radius-md);
    background-color: var(--color-surface-container);
    color: var(--color-text);
    box-shadow: var(--shadow-lg);

    /* Off-screen rather than display:none, so it is still focusable. */
    transform: translateY(calc(-100% - var(--space-4)));
    transition: transform var(--motion-duration-fast) var(--motion-easing-standard);
}

.skip-link:focus-visible {
    transform: translateY(0);
}

/* -----------------------------------------------------------------------------
   Sidebar
   ----------------------------------------------------------------------------- */

.app-sidebar {
    /* A drawer scrolled to its end must not start scrolling the page behind it -
       "scroll chaining" is the effect where closing a nav by flicking it also moves
       the content underneath, which reads as the screen having come apart. */
    overscroll-behavior: contain;
    position: sticky;
    top: 0;
    z-index: var(--z-sidebar);
    flex: 0 0 var(--layout-sidebar-width);
    height: 100vh;
    height: 100dvh;
    overflow-y: auto;
    background-color: var(--color-sidebar-background);
    color: var(--color-sidebar-text);
    border-right: var(--border-width) solid var(--color-sidebar-border);

    /* Clears a notch when the device is held in landscape. */
    padding-left: var(--layout-safe-left);
    padding-top: var(--layout-safe-top);
    padding-bottom: var(--layout-safe-bottom);
}

/* -----------------------------------------------------------------------------
   Main column
   ----------------------------------------------------------------------------- */

.app-main {
    flex: 1 1 auto;
    /* Without this, a wide table inside a flex child stretches the whole shell
       instead of scrolling within its own container. */
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.app-topbar {
    position: sticky;
    top: 0;
    z-index: var(--z-topbar);
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-height: var(--layout-topbar-height);
    padding: 0 var(--space-5);
    padding-top: var(--layout-safe-top);
    padding-right: calc(var(--space-5) + var(--layout-safe-right));
    background-color: var(--color-topbar-background);
    border-bottom: var(--border-width) solid var(--color-border);
}

/* Pushes the account controls to the trailing edge, leaving the leading edge for
   the drawer toggle. A spacer rather than justify-content, so items on either
   side keep their natural order for a screen reader. */
.app-topbar__spacer {
    flex: 1 1 auto;
}

.app-content {
    flex: 1 1 auto;
    width: 100%;
    max-width: var(--layout-content-max-width);
    /* Centred once the window is wider than the cap, rather than clinging to the
       sidebar with an ocean of empty space on the right. */
    margin-inline: auto;
    padding: var(--space-6) var(--space-5);
    padding-bottom: calc(var(--space-6) + var(--layout-safe-bottom));
}

/* -----------------------------------------------------------------------------
   Drawer toggle and overlay - only meaningful below the breakpoint
   ----------------------------------------------------------------------------- */

.app-nav-toggle {
    display: none;
    align-items: center;
    justify-content: center;
    width: var(--layout-touch-target-min);
    min-width: var(--layout-touch-target-min);
    padding: 0;
    border-radius: var(--radius-md);
}

.app-overlay {
    display: none;
    position: fixed;
    inset: 0;
    /* A button, not a div: dismissing the drawer has to be reachable without a
       pointer, and Escape alone is not discoverable. */
    border: 0;
    padding: 0;
    background-color: var(--color-overlay);
    cursor: pointer;
}

/* -----------------------------------------------------------------------------
   Mobile and tablet-portrait layout
   ----------------------------------------------------------------------------- */

@media (max-width: 64rem) {
    .app-nav-toggle {
        display: inline-flex;
    }

    .app-sidebar {
        position: fixed;
        inset: 0 auto 0 0;
        /* Never the full width: leaving the page edge visible is what makes it
           read as a layer over the page rather than a new screen. */
        width: min(20rem, 84vw);
        flex-basis: auto;
        /* Above the top bar and the overlay: the drawer is modal here. */
        z-index: calc(var(--z-modal) + 1);
        transform: translateX(-100%);
        transition: transform var(--motion-duration-base) var(--motion-easing-standard);
        box-shadow: var(--shadow-lg);
    }

    .app-shell--nav-open .app-sidebar {
        transform: translateX(0);
    }

    .app-overlay {
        display: block;
        z-index: var(--z-modal);
        opacity: 0;
        visibility: hidden;
        transition:
            opacity var(--motion-duration-base) var(--motion-easing-standard),
            visibility var(--motion-duration-base) step-end;
    }

    .app-shell--nav-open .app-overlay {
        opacity: 1;
        visibility: visible;
        transition:
            opacity var(--motion-duration-base) var(--motion-easing-standard),
            visibility 0s;
    }

    .app-content {
        padding: var(--space-5) var(--space-4);
        padding-bottom: calc(var(--space-5) + var(--layout-safe-bottom));
    }

    .app-topbar {
        padding-inline: var(--space-4);
        padding-right: calc(var(--space-4) + var(--layout-safe-right));
    }
}

/* Stops the page behind the drawer from scrolling. :has() is unsupported only in
   browsers this application does not target; where it is missing the background
   simply scrolls, which is untidy rather than broken. */
body:has(.app-shell--nav-open) {
    overflow: hidden;
}

/* A drawer that slides is orientation; a drawer that slides for someone who asked
   for less motion is just slower. Snap instead. */
@media (prefers-reduced-motion: reduce) {
    .app-sidebar,
    .app-overlay,
    .skip-link {
        transition: none;
    }
}

/* -----------------------------------------------------------------------------
   Blazor WebAssembly bootstrap UI
   Shown by index.html before the .NET runtime has finished downloading, which on
   a poor site connection can take a while - so it is worth looking deliberate.
   ----------------------------------------------------------------------------- */

.loading-progress {
    position: relative;
    display: block;
    width: 8rem;
    height: 8rem;
    margin: 20vh auto 1rem;
}

.loading-progress circle {
    fill: none;
    stroke: var(--color-surface-inset);
    stroke-width: 0.6rem;
    transform-origin: 50% 50%;
    transform: rotate(-90deg);
}

.loading-progress circle:last-child {
    stroke: var(--color-primary);
    stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
    transition: stroke-dasharray var(--motion-duration-base) var(--motion-easing-standard);
}

.loading-progress-text {
    position: absolute;
    text-align: center;
    font-weight: var(--font-weight-medium);
    inset: calc(20vh + 3.25rem) 0 auto 0;
    color: var(--color-text-muted);
}

.loading-progress-text::after {
    content: var(--blazor-load-percentage-text, "Loading");
}

#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: var(--z-toast);
    padding: var(--space-3) var(--space-5);
    padding-bottom: calc(var(--space-3) + var(--layout-safe-bottom));
    background-color: var(--color-warning-subtle);
    color: var(--color-text);
    box-shadow: var(--shadow-lg);
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: var(--space-3);
    top: var(--space-2);
}

/* =============================================================================
   Chart category colours
   -----------------------------------------------------------------------------
   GLOBAL, and deliberately so. The bar list, the pie and the heatmap draw the same
   breakdown in the same order, and a reader moving between them matches by colour -
   so Piping must be the same colour in all three. Component-scoped CSS cannot be
   shared, and three copies of eight colours is three chances for them to drift.

   Each class sets one custom property rather than a background: a chart applies it
   to whatever the shape happens to be - a bar fill, a pie segment, a legend swatch -
   without this file having to know which. See Components/Charts/ChartPalette.cs for
   which index gets which class, and why only seven of the eight are handed out.
   ============================================================================= */
.chart-cat-1 { --chart-colour: var(--color-category-1); }
.chart-cat-2 { --chart-colour: var(--color-category-2); }
.chart-cat-3 { --chart-colour: var(--color-category-3); }
.chart-cat-4 { --chart-colour: var(--color-category-4); }
.chart-cat-5 { --chart-colour: var(--color-category-5); }
.chart-cat-6 { --chart-colour: var(--color-category-6); }
.chart-cat-7 { --chart-colour: var(--color-category-7); }
.chart-cat-8 { --chart-colour: var(--color-category-8); }

/* =============================================================================
   Firm colours - the site map's dots and the legend under them
   -----------------------------------------------------------------------------
   GLOBAL for the same reason the chart categories above are, and SEPARATE from
   them for the opposite one.

   Global, because the map draws each firm in two places that are not the same
   element: a filled SVG circle inside the frame and an HTML swatch in the legend
   beneath it. A scoped stylesheet cannot be shared, and a dot and a swatch
   disagreeing about a firm's colour is the exact failure the legend exists to
   prevent - it would be a picture claiming a subcontractor was somewhere they
   were not.

   Separate, because a firm is not a row of the breakdown the bars, the pie and
   the heatmap draw. It appears in none of the three, its legend is its own, and
   nothing asks a reader to match a dot against a slice. Sharing the eight above
   only ever meant that the ninth firm on site wore the first firm's colour while
   the legend named both.

   The SAME --chart-colour custom property, deliberately. The map's scoped rules
   already read it - .sitemap__mark fills with it, .sitemap__swatch is backgrounded
   with it - so this file can hand the map a different palette without the
   component changing at all, and there is no second property name for the two to
   drift apart on. See Features/Reports/Map/FirmPalette.cs for which slot gets
   which class, why it does not wrap past the end, and what the two neutrals mean.
   ============================================================================= */
.firm-cat-1 { --chart-colour: var(--color-firm-1); }
.firm-cat-2 { --chart-colour: var(--color-firm-2); }
.firm-cat-3 { --chart-colour: var(--color-firm-3); }
.firm-cat-4 { --chart-colour: var(--color-firm-4); }
.firm-cat-5 { --chart-colour: var(--color-firm-5); }
.firm-cat-6 { --chart-colour: var(--color-firm-6); }
.firm-cat-7 { --chart-colour: var(--color-firm-7); }
.firm-cat-8 { --chart-colour: var(--color-firm-8); }
.firm-cat-9 { --chart-colour: var(--color-firm-9); }
.firm-cat-10 { --chart-colour: var(--color-firm-10); }

/* The bucket and the blank, which are two different statements and so are two
   different greys. Every firm past the tenth wears the first and the legend says
   how many are in it; somebody whose employer was never recorded wears the second,
   and it must not be the same colour because no chip can honestly count them as a
   firm. A missing rule here is not an error anybody sees, and it does not turn the
   dots black either: both readers of this property write it as
   var(--chart-colour, var(--color-accent)), so an undefined slot falls back to the
   accent - the marine the halo and the connecting lines already use. That is the
   worse failure, not the lesser one. Several firms would converge on a colour that
   looks deliberate and that the reader has already learned means something else,
   while the legend went on naming them separately. FirmPaletteTests asserts every
   class the palette can emit is defined here for exactly that reason. */
.firm-cat-other { --chart-colour: var(--color-firm-other); }
.firm-cat-none { --chart-colour: var(--color-firm-unknown); }
