/* ────────────────────────────────────────────────────────────────────────
 * The shell: the frame and its two cards, the sidebar's collapse, the
 * narrow layout's bar and drop-down panel, and the content card's enter
 * transition. Each Beta shell links it as `/shell.css` from its own
 * `index.html`, in the <head>, before the bundle.
 *
 * That placement is load-bearing. A stylesheet that arrives after React
 * has mounted (the way `app-beta.css` is appended from `index.tsx`) lands
 * as a style recalculation in which the real values and their
 * `transition` turn up together, so the browser animates every element
 * from its unstyled defaults: labels fading *out* on load, icons sliding
 * in from the left. In the <head> it is render-blocking, every class below
 * exists before the first element is created, and a new element has no
 * previous style for a transition to start from.
 *
 * The same reasoning is why these are classes in a file and not `Box`
 * props: `Box` generates an Aphrodite class per render and injects it on a
 * microtask after the commit, which reopens that gap on every toggle.
 *
 * State is a modifier class (`--open`, `--closed`, `--visible`), toggled
 * from `lib/web/components/Shell/motion.tsx`. Per-app configuration that
 * never changes (the rail's open width, a wordmark's gap) is a custom
 * property set once on the element. Nothing animated is ever an inline
 * value.
 *
 * Everything here is deliberately short. These are navigation chrome
 * animations, not flourishes: they exist to show where something went,
 * and anything long enough to notice as an animation is too long.
 * ──────────────────────────────────────────────────────────────────────── */

:root {
    /* Sidebar rail open/close. `motion.tsx` reads this for its timers. */
    --shell-collapse-ms: 240ms;

    /* Content appearing, after it has settled. `motion.tsx` reads this. */
    --shell-content-ms: 170ms;

    /* Labels fading with the rail: shorter than the rail, and offset on the
       way open so the fade finishes with the rail rather than trailing. */
    --shell-label-ms: 140ms;

    /* How long a label waits before fading in. The rail has to be most of
       the way open first, or the text is fading up into space that is not
       there yet and the two motions read as one smear. Fading out has no
       delay: the text goes first, then the rail closes over where it was. */
    --shell-label-delay-ms: calc(
        var(--shell-collapse-ms) - var(--shell-label-ms)
    );

    /* The contents dim through the middle of the slide: down over the first
       half, up over the second. `motion.tsx` halves the collapse itself. */
    --shell-dip-ms: calc(var(--shell-collapse-ms) / 2);
    --shell-dip-opacity: 0.75;

    /* Ease-out: leaves immediately, settles gently. Matches Blueprint. */
    --shell-ease: cubic-bezier(0.2, 0, 0, 1);

    /* The closed rail, and the icon-only square each item narrows to. The
       collapsed inset is derived from these, so there is nothing else to
       keep in step. */
    --shell-rail-closed-width: 62px;
    --shell-icon-size: 32px;

    /* The shell arriving on first load: the sidebar card, then the content
       card a beat behind it. Longer than anything else here on purpose; it
       runs once, and it is the one moment a slower fade reads as composure
       rather than lag. */
    --shell-arrive-ms: 420ms;
    --shell-arrive-stagger-ms: 90ms;

    /* Where the arrival starts from. Not from nothing: a card fading up from
       blank is too big a change to read as the page settling, so it starts
       half there and grows the last fraction of a percent into place. */
    --shell-arrive-from-opacity: 0.75;
    --shell-arrive-from-scale: 0.9925;

    /* The page content's first fade, inside the arriving card. Longer than
       a section change, which has a page already on screen to leave from. */
    --shell-content-initial-ms: 320ms;

    /* A breadcrumb arriving in the bar. */
    --shell-crumb-ms: 170ms;

    /* The narrow layout's bar, and the gutter around the cards. */
    --shell-bar-height: 56px;
    --shell-gutter: 8px;
}

/* ── Frame ─────────────────────────────────────────────────────────────
 * The plane the two cards sit on. A row of [sidebar, content] normally;
 * below the large breakpoint a column of [bar, content], where the sidebar
 * card has become a bar with a panel that drops down from it and pushes
 * the content card down. `ShellFrame` decides which from `@web/media`. */
.shell-frame {
    /* Absolute against the app container rather than `height: 100%`: a
       percentage only resolves if every ancestor has a definite height, and
       the shells are mounted under redirect and boundary wrappers that make
       no such promise. `inset: 0` gives the frame a definite height by
       construction, which is what lets the sidebar's menu scroll between its
       pinned header and footer instead of growing the page. */
    position: absolute;
    inset: 0;
    box-sizing: border-box;
    display: flex;
    padding: var(--shell-gutter);
    gap: var(--shell-gutter);
}

/* The sidebar card is a column whatever the layout, so the rail inside it
   can be sized by flex rather than by a percentage of a stretched flex item,
   which browsers do not always treat as definite. */
.shell-card--sidebar {
    flex-direction: column;
    min-height: 0;
}

.shell-frame--mobile {
    flex-direction: column;
}

/* The rail is no longer a rail: full width, as tall as its menu. The
   sidebar inside it is forced open, so `.shell-rail--open` is what applies
   and only the widths need undoing. */
.shell-frame--mobile .shell-rail {
    width: 100%;
    min-width: 0;
    max-width: none;
    flex: none;
}

/* And so are its items: the open rail's width is no longer a cap. */
.shell-frame--mobile .shell-rail-item {
    max-width: none;
}

/* ── Bar ───────────────────────────────────────────────────────────────
 * The narrow layout's top bar: the app's brand, and a chevron that opens
 * the menu beneath. The chevron turns over as the panel opens. */
.shell-bar {
    box-sizing: border-box;
    display: flex;
    align-items: center;
    gap: var(--shell-gutter);
    height: var(--shell-bar-height);
    flex-shrink: 0;
    padding: 0 var(--shell-gutter) 0 14px;
}

.shell-bar__brand {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
}

.shell-bar__toggle .bp5-icon {
    transition: transform var(--shell-collapse-ms) var(--shell-ease);
}

.shell-bar--open .shell-bar__toggle .bp5-icon {
    transform: rotate(180deg);
}

/* The drop-down panel is a `.shell-collapse-y` row, and this is its item:
   the non-visible overflow is what lets the `0fr` row reach zero, and it
   doubles as the scroll container for a menu taller than the viewport. */
.shell-bar__panel {
    overflow-y: auto;
    max-height: 70vh;
}

/* ── Breadcrumbs ───────────────────────────────────────────────────────
 * A crumb slides in from the left as it registers. An animation rather
 * than a transition because it runs on creation: a new element has no
 * previous style to transition from, but an animation needs none.
 * Enter only. Leaving would mean holding the crumb mounted after the page
 * that owned it has gone, which is a change to the bar's state model. */
@keyframes shell-crumb-in {
    from {
        opacity: 0;
        transform: translateX(-4px);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

.shell-crumb {
    animation: shell-crumb-in var(--shell-crumb-ms) var(--shell-ease);
}

/* Going up a level: the crumb that was deepest slides back out the way it
   came and fades, held in the bar for exactly one animation by `Menu`
   before it is dropped. `forwards` keeps it at the end state for the frame
   between the animation finishing and React removing it. */
@keyframes shell-crumb-out {
    from {
        opacity: 1;
        transform: none;
    }
    to {
        opacity: 0;
        transform: translateX(-4px);
    }
}

.shell-crumb--leaving {
    animation: shell-crumb-out var(--shell-crumb-ms) var(--shell-ease) forwards;
    pointer-events: none;
}

/* ── Link tabs ─────────────────────────────────────────────────────────
 * `LinkTabs` renders Blueprint's own indicator markup, positioned by
 * measuring the selected tab, so Blueprint's stylesheet animates it exactly
 * as it does for `Tabs`. The list only needs to be the indicator's
 * containing block. */
.shell-link-tabs {
    position: relative;
}

/* ── Rail ──────────────────────────────────────────────────────────────
 * The element whose width the shell card sizes itself by, and the one
 * that slides. The app's own rail sits inside at `width: 100%`. The shell
 * card wrapping this is a flex item sized by its content, so it follows
 * frame by frame without a transition of its own.
 *
 * `--shell-rail-open-width` is set on the element by `Rail`, once. */
.shell-rail {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    width: var(--shell-rail-width);
    min-width: var(--shell-rail-width);
    max-width: var(--shell-rail-width);
    transition:
        width var(--shell-collapse-ms) var(--shell-ease),
        min-width var(--shell-collapse-ms) var(--shell-ease),
        max-width var(--shell-collapse-ms) var(--shell-ease);
}

.shell-rail--open {
    --shell-rail-width: var(--shell-rail-open-width);
}

.shell-rail--closed {
    --shell-rail-width: var(--shell-rail-closed-width);
}

/* ── Rail contents ─────────────────────────────────────────────────────
 * Dims through the middle of the slide so the collapse is not purely
 * geometric. On the contents, never the rail: the rail paints the card's
 * background, and dimming that lightens it against the plane behind.
 * Returning to full strength is a transition to no `opacity` at all, so
 * nothing is left behind: a non-1 opacity is a containing block for the
 * `position: fixed` Blueprint portals use. */
.shell-rail-dip {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    transition: opacity var(--shell-dip-ms) var(--shell-ease);
}

.shell-rail-dip--dipping {
    opacity: var(--shell-dip-opacity);
}

/* ── Menu item geometry ────────────────────────────────────────────────
 * The box that narrows to the icon's square. Both states are concrete
 * lengths on purpose: a transition only runs between two values it can
 * interpolate, and `max-width: none` and `margin: auto` are neither, so
 * an item written that way jumps to the middle in one frame while the
 * rail is still closing.
 *
 * The open width is the rail's own, inherited from `.shell-rail--open`.
 * An item's static dressing (spacing, a heading's rule) is the app's
 * business and lives in its own stylesheet. */
.shell-rail-item {
    transition:
        max-width var(--shell-collapse-ms) var(--shell-ease),
        margin var(--shell-collapse-ms) var(--shell-ease),
        padding var(--shell-collapse-ms) var(--shell-ease);
}

.shell-rail-item--open {
    max-width: var(--shell-rail-open-width);
    margin-left: 0;
    margin-right: 0;
}

.shell-rail-item--closed {
    max-width: var(--shell-icon-size);
}

/* Platform Admin centres its closed items in the rail. Left at full width
   they keep the open item's left-aligned padding, so the icons sit off to
   one side with dead space beside them. */
.shell-rail-item--centred.shell-rail-item--closed {
    margin-left: calc(
        (var(--shell-rail-closed-width) - var(--shell-icon-size)) / 2
    );
    margin-right: calc(
        (var(--shell-rail-closed-width) - var(--shell-icon-size)) / 2
    );
}

/* The Management Portal and RAMP nudge a closed section heading instead. */
.shell-rail-item--nudged.shell-rail-item--closed {
    margin-left: 4px;
}

/* Platform Admin's Blueprint `Menu`. Its side padding follows the rail:
   closed, the items centre themselves, and a side padding here would push
   them off-centre by half of it. `min-width: 0` undoes Blueprint's 180px
   menu minimum, which is wider than the closed rail: the items would centre
   against 180px and land outside the 62px the rail shows. Two classes deep
   so this outranks `.bp5-menu` whatever order the stylesheets load in. */
.shell-rail .shell-rail-menu {
    width: 100%;
    min-width: 0;
    background: none;
    padding: 12px 8px;
    transition: padding var(--shell-collapse-ms) var(--shell-ease);
}

.shell-rail .shell-rail-menu--closed {
    padding-left: 0;
    padding-right: 0;
}

/* A section's nested items in the Management Portal and RAMP. These used
   to be `Box` props, which regenerated a class on every toggle. */
.shell-rail-nested {
    transition:
        margin-left var(--shell-collapse-ms) var(--shell-ease),
        padding-left var(--shell-collapse-ms) var(--shell-ease);
}

.shell-rail-nested--open {
    padding-left: 8px;
}

.shell-rail-nested--ruled.shell-rail-nested--open {
    margin-left: 12px;
    border-left: 1px solid #e5e7eb;
}

/* ── Labels ────────────────────────────────────────────────────────────
 * Always rendered, in both states: a label can only cross-fade if it is
 * still in the tree to fade. Closed, it sits at zero opacity inside an
 * item that has narrowed to the icon's square, clipped as well as
 * invisible.
 *
 * Only for hosts that will shrink it. Blueprint's `MenuItem` puts its text
 * in a `.bp5-fill` container, which does; a `Button` does not, and
 * `.bp5-button > *` is `flex-shrink: 0`, so a label left at full width in
 * a centred button shoves the icon off the edge. Buttons want the
 * collapsing variant below, which gives up its width rather than its ink.
 *
 * `nowrap` matters more than it looks: without it the label re-wraps onto
 * two lines as the rail narrows, and the item's height jumps mid-fade.
 * `text-overflow` is here because Blueprint's `ellipsizeText` only reaches
 * `.bp5-button-text`, which a label passed as children never lands in. */
.shell-fade {
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.shell-fade,
.shell-collapse-x__clip,
.shell-collapse-y__clip,
.shell-icon-label__clip,
.shell-bar__panel {
    transition: opacity var(--shell-label-ms) var(--shell-ease);
}

.shell-fade--hidden {
    opacity: 0;
}

/* The delay lives on the visible state only: it is the after-change style
   that decides, so fading in waits and fading out does not. */
.shell-fade--visible {
    opacity: 1;
    transition-delay: var(--shell-label-delay-ms);
}

/* ── Collapsing width ──────────────────────────────────────────────────
 * For the wordmark beside a logo mark, and the footer buttons' text:
 * anything that has to stop taking up horizontal room, not just go
 * invisible. A merely-invisible wordmark keeps its width and holds the
 * centred logo permanently off-centre.
 *
 * A `0fr`/`1fr` grid column animates to the content's natural width with
 * nobody hardcoding what it is. `overflow: hidden` on the clip is
 * load-bearing: a grid item's automatic minimum size is its content unless
 * it has a non-visible overflow, so without it the `0fr` track would refuse
 * to go below the content's width.
 *
 * `--shell-gap`: space between this and whatever sits before it, inside the
 * clipped area on purpose. Left on the parent as a flex `gap`, it would
 * survive the collapse as a stray sliver.
 *
 * Not for a `Button`: Blueprint reaches into a button's children with
 * margins and pull-outs that this cannot see. Button text collapses through
 * `.shell-icon-label` below. */
.shell-collapse-x {
    display: grid;
    transition: grid-template-columns var(--shell-collapse-ms) var(--shell-ease);
}

.shell-collapse-x--open {
    grid-template-columns: 1fr;
}

.shell-collapse-x--closed {
    grid-template-columns: 0fr;
}

.shell-collapse-x__clip {
    overflow: hidden;
    text-overflow: ellipsis;
}

.shell-collapse-x__content {
    padding-left: var(--shell-gap, 0px);
    white-space: nowrap;
}

/* ── Collapsing height ─────────────────────────────────────────────────
 * Platform Admin's group headings: the title collapses away with the rail,
 * over a rule that does not. Closed to icons, a rail with no rules between
 * its groups is one undivided column of glyphs. Keeping the rule also
 * halves what has to move: only the title's height changes, so the icons
 * below it travel a shorter distance.
 *
 * Blueprint's own `.bp5-menu-header` border never draws here: wrapping the
 * divider makes its `li` permanently `:first-of-type`, which Blueprint uses
 * to suppress the first border. The rule is drawn on the wrapper instead. */
.shell-group--ruled {
    border-top: 1px solid rgba(17, 20, 24, 0.15);
    margin-top: 8px;
    padding-top: 8px;
}

.bp5-dark .shell-group--ruled {
    border-top-color: rgba(255, 255, 255, 0.12);
}

.shell-collapse-y {
    display: grid;
    transition: grid-template-rows var(--shell-collapse-ms) var(--shell-ease);
}

.shell-collapse-y--open {
    grid-template-rows: 1fr;
}

.shell-collapse-y--closed {
    grid-template-rows: 0fr;
}

.shell-collapse-y__clip {
    overflow: hidden;
    white-space: nowrap;
}

/* ── Content ───────────────────────────────────────────────────────────
 * Opacity only, deliberately no `transform`: a transform makes this the
 * containing block for absolutely positioned descendants, and `PageWrapper`
 * is `position: absolute; inset: 0`, so every page using one would reparent
 * onto a box that measures zero and vanish. Once landed, no modifier at
 * all, since a non-1 opacity is a containing block for `position: fixed`. */
.shell-content {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-width: 0;
    min-height: 0;
}

.shell-content--entering {
    opacity: 0;
}

.shell-content--shown {
    opacity: 1;
    transition: opacity var(--shell-content-ms) var(--shell-ease);
}

.shell-content--shown.shell-content--initial {
    transition-duration: var(--shell-content-initial-ms);
}

/* ── Icon + collapsing label ───────────────────────────────────────────
 * The content of a Button whose text collapses with the rail: the footer's
 * My Account and Collapse Sidebar.
 *
 * It is the button's *only* child, and that is the fix for the icons that
 * sat off-centre when closed. Blueprint lays a button out as a centred flex
 * row and reaches into it: `.bp5-button > *` is `flex-shrink: 0` with a
 * trailing margin, and a lone icon is pulled out by 7px a side. Any label
 * placed beside the icon as a sibling is subject to all of that, and the
 * arithmetic to cancel it depended on exactly which rule fired. One child
 * means none of them do. Closed, it is `padding-left: calc(50% - 8px)`
 * and the icon alone, so the icon's centre is the button's centre by
 * construction. Open, the padding goes to zero and the label fills the
 * rest, so the row is left-aligned like the nav items above it, with the
 * caret at the far right. Percent padding interpolates, so the icon glides
 * from the middle to the left edge as the label grows.
 *
 * `--shell-icon-label-icon` is the icon's width, for the centring. */
.shell-icon-label {
    --shell-icon-label-icon: 16px;
    box-sizing: border-box;
    display: grid;
    grid-template-columns: max-content var(--shell-icon-label-col);
    align-items: center;
    width: 100%;
    transition:
        grid-template-columns var(--shell-collapse-ms) var(--shell-ease),
        padding-left var(--shell-collapse-ms) var(--shell-ease);
}

.shell-icon-label--open {
    --shell-icon-label-col: 1fr;
    padding-left: 0;
}

.shell-icon-label--closed {
    --shell-icon-label-col: 0fr;
    padding-left: calc(50% - var(--shell-icon-label-icon) / 2);
}

.shell-icon-label__clip {
    overflow: hidden;
    min-width: 0;
}

.shell-icon-label__text {
    display: flex;
    align-items: center;
    gap: 7px;
    padding-left: 7px;
    white-space: nowrap;
}

/* The label gives way before the caret does, and trails off rather than
   being cut mid-glyph: a long organisation name in the Management Portal. */
.shell-icon-label__label {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ── Arrival ───────────────────────────────────────────────────────────
 * The two shell cards on first mount: each fades in and expands the last
 * fraction of a percent from its centre, the sidebar first and the content
 * card a beat behind it. Kept slight: a quarter of the opacity and under a
 * percent of scale. Inside the content card, the page's own content then
 * fades up once it has settled, so the whole thing arrives in stages:
 * cards, then bar, then page.
 *
 * The content card is `position: relative` already, which is what makes a
 * transform safe on it: it was the containing block for the page's
 * absolutely positioned wrappers before, and still is. The sidebar card has
 * no positioned descendants that depend on it; its popovers portal out.
 * Once landed there is no modifier at all, so nothing is left on either
 * card to catch a `position: fixed` descendant. */
.shell-card--arriving {
    opacity: var(--shell-arrive-from-opacity);
    transform: scale(var(--shell-arrive-from-scale));
    transform-origin: center;
}

.shell-card--arrived {
    opacity: 1;
    transform: none;
    transition:
        opacity var(--shell-arrive-ms) var(--shell-ease),
        transform var(--shell-arrive-ms) var(--shell-ease);
}

.shell-card--arrived.shell-card--content {
    transition-delay: var(--shell-arrive-stagger-ms);
}

/* ── Table footer, folded ──────────────────────────────────────────────
 * A footer too narrow for its controls keeps the count or the pager and
 * folds everything else into one Options popover (`NewTable/Footer`). The
 * popover holds the very same buttons the wide footer shows, so they have
 * to be told to behave as a column: fill it, left-align, and stack any
 * row of actions a page composed for the wide layout. */
.new-table-footer-options {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 4px;
    min-width: 220px;
}

.new-table-footer-options .bp5-popover-target {
    display: block;
}

.new-table-footer-options .bp5-button {
    width: 100%;
    justify-content: flex-start;
}

.new-table-footer-options .new-table-footer-actions {
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
}

/* ── Reduced motion ────────────────────────────────────────────────────
 * Nothing above moves for users who have asked their OS for less motion.
 * Last in the file so it wins on source order at equal specificity. */
@media (prefers-reduced-motion: reduce) {
    .shell-rail,
    .shell-rail-dip,
    .shell-rail-item,
    .shell-rail .shell-rail-menu,
    .shell-rail-nested,
    .shell-fade,
    .shell-fade--visible,
    .shell-collapse-x,
    .shell-collapse-x__clip,
    .shell-collapse-y,
    .shell-collapse-y__clip,
    .shell-icon-label,
    .shell-icon-label__clip,
    .shell-card--arrived,
    .shell-content--shown,
    .shell-bar__panel,
    .shell-bar__toggle .bp5-icon,
    .shell-link-tabs .bp5-tab-indicator-wrapper {
        transition: none;
    }

    .shell-crumb,
    .shell-crumb--leaving {
        animation: none;
    }
}
