/*
 * Nautilus — the Stocktilus design system.
 *
 * Dark ground, one accent per surface, the number as the hero, chunky
 * geometry, glow instead of shadow. The retro layer (pixel type, hard
 * shadows, scanlines) is opt-in via modifier classes so it can never land on
 * a price table by accident.
 *
 * Loaded AFTER bootstrap.min.css, which is still present for the pages that
 * have not been migrated yet. Both come out together at the end.
 */

/* ---------------------------------------------------------------- fonts */

/* Silkscreen (SIL Open Font License 1.1) — display sizes only, >= 24px. */
@font-face {
    font-family: "Silkscreen";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/thirdparty/fonts/silkscreen-400-latin.woff2") format("woff2");
}

@font-face {
    font-family: "Silkscreen";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("/thirdparty/fonts/silkscreen-700-latin.woff2") format("woff2");
}

/* --------------------------------------------------------------- tokens */

:root {
    /* surfaces */
    --surface-0: #0B0B10;
    --surface-1: #14141C;
    --surface-2: #1E1E2A;
    --border: #2A2A3A;

    /* ink */
    --ink-0: #F2F3F5;
    --ink-1: #B5BAC1;
    --ink-2: #80848E;

    /* accent */
    --accent: #5865F2;
    --accent-ink: #FFFFFF;
    --accent-glow: rgba(88, 101, 242, .35);
    /*
     * Blurple itself is only 4.26:1 on --surface-0, under the AA floor for
     * body text. Fills and borders may use --accent; anything that is text
     * uses this lighter tint (7.4:1).
     */
    --accent-link: #8A94FF;

    /* semantic — gains and losses ONLY, never generic UI accents */
    --up: #57F287;
    --down: #ED4245;
    --warn: #FEE75C;

    /* geometry */
    --radius-card: 20px;
    --radius-ctl: 12px;
    --pixel-step: 4px;

    /* elevation */
    --shadow-hard: 4px 4px 0 var(--border);
    --shadow-glow: 0 0 24px var(--accent-glow);

    /* space scale */
    --s1: 4px;
    --s2: 8px;
    --s3: 12px;
    --s4: 16px;
    --s5: 24px;
    --s6: 32px;
    --s7: 48px;
    --s8: 64px;

    /* retro */
    --scanline: rgba(255, 255, 255, .03);

    /*
     * Type has three roles. Display is the only self-hosted face; UI and data
     * ride the system stacks for now (see docs/design/typography.md when it
     * lands). A bitmap face cannot carry prices — it collapses below 24px.
     */
    --font-display: "Silkscreen", ui-monospace, monospace;
    --font-ui: system-ui, -apple-system, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;
    --font-data: ui-monospace, "Cascadia Mono", "Segoe UI Mono", "SF Mono", "IBM Plex Mono", Menlo, Consolas, monospace;
}

/* ----------------------------------------------------------------- base */

*,
*::before,
*::after {
    box-sizing: border-box;
}

body {
    margin: 0;
    min-height: 100vh;
    background-color: var(--surface-0);
    color: var(--ink-0);
    font-family: var(--font-ui);
    font-size: 15px;
    line-height: 1.55;
    -webkit-font-smoothing: antialiased;
}

/* Page content that is not yet inside a migrated layout still needs air. */
body > h1,
body > h2,
body > h3,
body > h4,
body > p,
body > a,
body > form,
body > table,
body > div:not([class]) {
    margin-left: var(--s5);
    margin-right: var(--s5);
}

h1, h2, h3, h4, h5, h6 {
    color: var(--ink-0);
    font-weight: 700;
    line-height: 1.15;
    margin: var(--s6) 0 var(--s4);
}

h1 {
    font-size: 30px;
    letter-spacing: -.02em;
}

h2 {
    font-size: 23px;
}

h3 {
    font-size: 18px;
    color: var(--ink-1);
    text-transform: uppercase;
    letter-spacing: .08em;
}

a {
    color: var(--accent-link);
    text-decoration: none;
}

a:hover {
    color: var(--ink-0);
    text-decoration: underline;
}

hr {
    border: 0;
    border-top: 1px solid var(--border);
    margin: var(--s5) 0;
}

code, kbd, pre, samp {
    font-family: var(--font-data);
}

/* Bootstrap ships #e83e8c for inline code, which is off-palette. */
code {
    color: var(--accent-link);
    background: var(--surface-2);
    padding: 1px 6px;
    border-radius: 6px;
    font-size: .92em;
}

label {
    color: var(--ink-1);
    font-size: 13px;
}

/* ------------------------------------------------------------- controls */

input[type="text"],
input[type="password"],
input[type="number"],
input[type="date"],
input[type="datetime-local"],
input[type="search"],
input[type="email"],
select,
textarea {
    background: var(--surface-2);
    color: var(--ink-0);
    border: 2px solid var(--border);
    border-radius: var(--radius-ctl);
    padding: 9px var(--s3);
    font-family: var(--font-ui);
    font-size: 15px;
    transition: border-color .12s, box-shadow .12s;
}

input[type="number"],
input[type="date"],
input[type="datetime-local"] {
    font-family: var(--font-data);
    font-variant-numeric: tabular-nums;
}

input:focus-visible,
select:focus-visible,
textarea:focus-visible,
button:focus-visible,
a:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: var(--shadow-glow);
}

input::placeholder,
textarea::placeholder {
    color: var(--ink-2);
}

button,
input[type="submit"],
input[type="button"] {
    background: var(--accent);
    color: var(--accent-ink);
    border: 2px solid var(--accent);
    border-radius: var(--radius-ctl);
    padding: 9px var(--s4);
    font-family: var(--font-ui);
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
    transition: transform .08s steps(2), filter .12s;
}

button:hover,
input[type="submit"]:hover {
    filter: brightness(1.12);
}

button:active,
input[type="submit"]:active {
    transform: translate(2px, 2px);
}

.button--ghost {
    background: transparent;
    color: var(--ink-1);
    border-color: var(--border);
}

.button--danger {
    background: transparent;
    color: var(--down);
    border-color: var(--down);
}

.button--danger:hover {
    background: rgba(237, 66, 69, .14);
    filter: none;
}

/* Busy state: the label makes way for a stepped spinner, width unchanged. */
.button--loading {
    position: relative;
    color: transparent;
    pointer-events: none;
}

.button--loading::after {
    content: "";
    position: absolute;
    inset: 0;
    margin: auto;
    width: 12px;
    height: 12px;
    border: 2px solid var(--ink-1);
    border-top-color: transparent;
    border-radius: 50%;
    animation: nautilus-spin .64s steps(8) infinite;
}

@keyframes nautilus-spin {
    to {
        transform: rotate(360deg);
    }
}

@media (prefers-reduced-motion: reduce) {
    .button--loading::after {
        animation: none;
    }
}

.button--ghost:hover {
    color: var(--ink-0);
    border-color: var(--ink-2);
    filter: none;
}

/* ------------------------------------------------------- progress steps */
/*
 * Segmented rather than continuous: one block per unit of work, so the bar says how many things
 * there are and which are done at the same time. Steps are also the honest shape here — the work
 * completes in discrete jumps, and a smoothly sliding bar would be inventing progress it cannot
 * know (one of these services takes 40s while the rest take microseconds).
 */

.progress-steps {
    display: flex;
    gap: 3px;
}

.progress-step {
    flex: 1 1 0;
    height: 14px;
    background: var(--surface-2);
    border: 2px solid var(--border);
    border-radius: 3px;
}

.progress-step--running {
    border-color: var(--accent);
    background: rgba(88, 101, 242, .25);
    animation: progress-pulse .8s steps(2) infinite alternate;
}

.progress-step--done {
    border-color: var(--accent);
    background: var(--accent);
}

.progress-step--failed {
    border-color: var(--down);
    background: var(--down);
}

@keyframes progress-pulse {
    to {
        background: rgba(88, 101, 242, .6);
    }
}

@media (prefers-reduced-motion: reduce) {
    .progress-step--running {
        animation: none;
    }
}

/* ------------------------------------------------------------ page lock */
/*
 * While a long action owns the page, every control goes inert. `disabled` on the buttons is what
 * actually blocks activation and keeps them out of the tab order; this only stops them looking
 * pressable.
 */

body.is-busy button:disabled,
body.is-busy input:disabled,
body.is-busy select:disabled {
    opacity: .45;
    cursor: not-allowed;
}

/* --------------------------------------------------------------- loading */
/*
 * Three devices, one per situation, drawn from the same instrument panel as the login hatch.
 *
 *   - `.button--loading` (above) — a control is working. Unchanged.
 *   - `.region-loader`             — a *region* is being replaced: a chart, a table, a card.
 *   - `.skeleton`                  — a region is being filled for the first time and has nothing
 *                                    to show underneath yet.
 *
 * The split matters because the two region cases are different facts. Bars already on the page are
 * still the last true answer until the next one lands, so a repeat request sweeps over them rather
 * than blanking them; a first request has nothing to preserve and shows where the content is going
 * to be instead.
 *
 * All of it is drawn, none of it is written — the string next to these stays "Fetching…" (see
 * docs/design/copy.md). And all of it is decorative: every animation here has a reduced-motion
 * counterpart at the end of the section, and the surface stays legible without them.
 */

/*
 * The sonar ping: the general-purpose spinner, for anywhere that needs one and is not a button.
 * Rings leave the centre and fade, in four visible stages rather than a smooth grow — the same
 * stepped motion as the button spinner and the hatch wheel (docs/design/retro.md).
 *
 * Needs about 32px to read; below that the rings collide and it turns into a smudge. Use the
 * button spinner at control size.
 */
.loader-sonar {
    --sonar-size: 40px;
    position: relative;
    display: inline-block;
    flex: none;
    width: var(--sonar-size);
    height: var(--sonar-size);
}

/* The transducer — the one part that does not move. */
.loader-sonar::before {
    content: "";
    position: absolute;
    inset: 0;
    margin: auto;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--accent);
}

/* Two rings on the same keyframes, half a cycle apart, so one is always outbound. */
.loader-sonar__ring {
    position: absolute;
    inset: 0;
    border: 2px solid var(--accent);
    border-radius: 50%;
    opacity: 0;
    animation: nautilus-sonar 1.6s steps(4) infinite;
}

.loader-sonar__ring:nth-child(2) {
    animation-delay: .8s;
}

@keyframes nautilus-sonar {
    from {
        transform: scale(.2);
        opacity: .9;
    }

    to {
        transform: scale(1);
        opacity: 0;
    }
}

/*
 * A region that is working. Applied with `Nautilus.busy()`, which is also what inserts the
 * `.region-loader` child — the class on its own only prepares the region to hold one.
 *
 * `position: relative` is the whole of what it does to layout: no offsets, so nothing moves.
 */
.is-loading {
    position: relative;
}

/*
 * The scrim over the stale content. Deliberately not opaque: the previous answer stays readable
 * underneath, greyed enough that nobody mistakes it for the new one.
 */
.region-loader {
    position: absolute;
    inset: 0;
    z-index: 2;
    overflow: hidden;
    background: rgba(11, 11, 16, .55);
    cursor: progress;
}

/*
 * The pane over a region that is showing a skeleton. No scrim and no device: the placeholder is the
 * content, so there is nothing to grey out, and its seams are already the moving part. What is left
 * is what is not decoration — it carries the status role and it keeps the pointer out of a region
 * whose contents are about to be replaced.
 */
.region-loader--clear {
    background: transparent;
}

.region-loader--sonar {
    display: flex;
    align-items: center;
    justify-content: center;
}

/*
 * The sounding line: one trace crossing the region at a stepped pace, the way a sounding is taken.
 * It says a request is out and nothing more — which is the honest claim, since none of these
 * vendors tell us how far along they are. A bar filling toward a percentage we cannot know is the
 * faux-progress device docs/design/retro.md rules out.
 */
.region-loader__line {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 2px;
    background: var(--accent);
    box-shadow: 0 0 12px var(--accent-glow);
    animation: nautilus-sounding 1.8s steps(18) infinite;
}

/* The wake, trailing behind the trace rather than around it, so the line has a direction. */
.region-loader__line::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    right: 2px;
    width: 56px;
    background: linear-gradient(to left, var(--accent-glow), transparent);
}

@keyframes nautilus-sounding {
    from {
        left: 0;
    }

    to {
        left: 100%;
    }
}

/*
 * Skeletons, drawn as hull plating: a flat plate with a weld seam running along it. The seam is a
 * hard-edged band moving in steps rather than the usual soft gradient sweep — a smooth shimmer is
 * the one piece of borrowed visual language that would look like it came from another design
 * system.
 *
 * A skeleton is a placeholder, never a value. Nothing in here may carry `--up` or `--down`: a ghost
 * row is not a gain.
 */
.skeleton {
    position: relative;
    overflow: hidden;
    background-color: var(--surface-2);
    border-radius: var(--radius-ctl);
}

/*
 * The seam travels as a background position rather than as an offset, because a table of ghost rows
 * puts fifty-odd of these on screen at once and animating `left` would relayout every one of them
 * on every step. Position is paint only.
 */
.skeleton::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image: linear-gradient(to right, transparent, rgba(138, 148, 255, .22), transparent);
    background-repeat: no-repeat;
    background-size: 32px 100%;
    background-position-x: -32px;
    animation: nautilus-seam 1.5s steps(15) infinite;
}

@keyframes nautilus-seam {
    from {
        background-position-x: -32px;
    }

    to {
        /* 100% lands the band's right edge on the plate's right edge; the rest carries it off. */
        background-position-x: calc(100% + 32px);
    }
}

/* The big plate — a chart, a panel. Bordered and riveted, because at this size a bare rectangle
   reads as a broken image rather than as something arriving. */
.skeleton--plate {
    border: 2px solid var(--border);
    background-image:
        radial-gradient(circle at 12px 12px, var(--border) 2px, transparent 2px),
        radial-gradient(circle at calc(100% - 12px) 12px, var(--border) 2px, transparent 2px),
        radial-gradient(circle at 12px calc(100% - 12px), var(--border) 2px, transparent 2px),
        radial-gradient(circle at calc(100% - 12px) calc(100% - 12px), var(--border) 2px, transparent 2px);
}

/* The small plate — one value's worth, sized to sit inside a table cell or a card field. */
.skeleton--bar {
    display: block;
    width: 100%;
    min-width: 40px;
    height: 12px;
    border-radius: 6px;
}

.skeleton-stack {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
}

.skeleton-stack .skeleton {
    height: 20px;
}

@media (prefers-reduced-motion: reduce) {
    /*
     * Every device here loses its motion and keeps its shape: one ring held at full extent, the
     * sounding line parked at the left edge, the seam gone. The region still reads as working from
     * the scrim, the cursor and `aria-busy`, none of which are animation.
     */
    .loader-sonar__ring {
        animation: none;
        opacity: .55;
        transform: scale(1);
    }

    .loader-sonar__ring:nth-child(2) {
        display: none;
    }

    .region-loader__line,
    .skeleton::after {
        animation: none;
    }

    .skeleton::after {
        display: none;
    }
}

/* ---------------------------------------------------------------- chips */
/*
 * Status pills. Note what is NOT here: a green "good" chip. Green and red are
 * reserved for gains and losses, so a chip may never borrow them — a config
 * key that is merely unset is not a loss.
 */

/*
 * Flat labels with a pixel status dot — no border, no fill, no pill. A chip with a 2px border and a
 * rounded background is the same shape as the buttons next to it, and reads as one; these have to
 * be obviously inert, since they sit in a column beside real controls.
 */
.chip {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--ink-2);
    font-family: var(--font-data);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
    white-space: nowrap;
}

.chip::before {
    content: "";
    width: 7px;
    height: 7px;
    flex: none;
    background: currentColor;
}

.chip--on {
    color: var(--ink-0);
}

.chip--on::before {
    background: var(--accent);
    box-shadow: 0 0 8px var(--accent-glow);
}

/* Stored encrypted. Not a warning — a "handle knowingly" marker. */
.chip--lock {
    color: var(--warn);
}

/* ------------------------------------------------------- layout helpers */
/*
 * Whitespace between two inline controls collapses to a single space, so a row
 * of <button>s written one per line renders as a row of buttons touching. These
 * give a real gap, and let the row wrap rather than force the page wider.
 */

.controls-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2);
}

/* For separating whole groups rather than the controls inside one. */
.controls-row--loose {
    gap: var(--s5);
}

/*
 * A run of content between the heading and the next one. Carries the side
 * margins that `body > div:not([class])` would have given it — adding a class
 * to a top-level div takes it out of that rule, which is easy to miss because
 * nothing looks broken until it sits next to something that still has them.
 */
.page-block {
    display: flex;
    flex-direction: column;
    gap: var(--s3);
    margin: var(--s4) var(--s5);
}

/* ---------------------------------------------------- candlesticks chart */
/*
 * The one piece of the chart that is DOM rather than canvas: the line naming
 * what is plotted. Written by `CandlesticksChart` from its `caption` option and
 * left as text so it can be selected, copied and found — a caption painted into
 * the canvas is pixels.
 */
.candlesticks-chart__caption {
    margin: 0 0 var(--s2);
    color: var(--ink-2);
    font-family: var(--font-data);
    font-size: 13px;
}

/*
 * The caption once it carries the visible range as well: what the chart is on
 * the left, what part of it you are looking at on the right. Wrapping rather
 * than shrinking, because both halves are facts and neither is worth
 * truncating to keep them on one line.
 */
.candlesticks-chart__caption--ranged {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 0 var(--s3);
}

/*
 * Brighter than the rest of the caption, on the same reasoning as the readout's
 * timestamp: this is the half that changes while you drag, and the eye should
 * be able to find it without hunting.
 */
.candlesticks-chart__visible-range {
    color: var(--ink-1);
}

/*
 * The hover readout: the OHLC and volume of the candle under the pointer.
 *
 * Same family as the caption and a step smaller — it is a running commentary on
 * the chart rather than a heading for it, and at the caption's 13px it competed
 * with the title above. Wraps rather than scrolls, so a narrow chart stacks the
 * figures instead of hiding them.
 */
.candlesticks-chart__readout {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s1) var(--s3);
    font-family: var(--font-data);
    font-size: 12px;
    line-height: 1.4;
}

/* The values are what is being read; the labels only say which is which. */
.candlesticks-chart__readout-value {
    color: var(--ink-0);
}

.candlesticks-chart__readout-label {
    margin-right: var(--s1);
    color: var(--ink-2);
}

.candlesticks-chart__readout-time {
    color: var(--ink-1);
}

/*
 * A chart drawn wider than the box it is in, which is what `minCandleWidth`
 * asks for once the candles need more room than there is: the scale figures in
 * a column that stays put, everything drawn in a scroller beside it.
 *
 * Only built when the drawing actually overflows — a chart that fits has its
 * canvases straight in the caller's element, with none of this around them.
 */
.candlesticks-chart__frame {
    display: flex;
    align-items: flex-start;
}

/*
 * The fixed column: the high and low prices, a panel's name, the top of the
 * volume scale. Sized to its canvases rather than by a width here — the chart
 * measures the figures it is about to draw, because `779.37` and `12,345,678`
 * want very different columns.
 */
.candlesticks-chart__gutter {
    flex: 0 0 auto;
    border-right: 1px solid var(--border);
}

/*
 * Every canvas of the chart, in one box, so the candles and the panels beneath
 * them cannot come out of step. `min-width: 0` because a flex item's floor is
 * its content, and without it the scroller would push the frame wider instead
 * of scrolling.
 */
.candlesticks-chart__scroller {
    flex: 1 1 auto;
    min-width: 0;
    overflow-x: auto;
    overflow-y: hidden;

    /*
     * No bar under the candles: the drawing is dragged by hand, which is what
     * the grab cursor is announcing. The box still scrolls — a trackpad, a
     * finger, and the chart's own "open at the recent end" all go through it —
     * it just has nothing drawn across the bottom of the chart.
     */
    scrollbar-width: none;
    -ms-overflow-style: none;
    cursor: grab;

    /*
     * Nothing in here is text, and a drag that starts on a canvas should move
     * the candles rather than select the caption above them.
     */
    -webkit-user-select: none;
    user-select: none;

    /*
     * How far the candles dissolve at an edge there is more chart beyond. Zero
     * on both by default: the modifiers below turn on the side that has
     * somewhere to go, so an edge you have reached the end of stays sharp.
     */
    --candles-fade-left: 0px;
    --candles-fade-right: 0px;
}

/*
 * The one thing that says the chart moves. The grab cursor only exists under a
 * pointer, and on a touchscreen there is no cursor at all — so without this, a
 * chart that scrolls looks exactly like a chart that does not.
 *
 * A mask rather than a gradient painted over the edges: this fades the candles
 * themselves to transparent, so it does not need to know what colour it is
 * sitting on. The same chart goes on `--surface-1` inside a collapsible section
 * and on `--surface-2` in a card, and an overlay hard-coded to either one shows
 * a seam on the other.
 *
 * Declared on the modifiers rather than the base so a chart parked at an end
 * with nothing beyond it carries no mask at all.
 */
.candlesticks-chart__scroller--more-left,
.candlesticks-chart__scroller--more-right {
    -webkit-mask-image: linear-gradient(
            to right,
            transparent 0,
            #000 var(--candles-fade-left),
            #000 calc(100% - var(--candles-fade-right)),
            transparent 100%);
    mask-image: linear-gradient(
            to right,
            transparent 0,
            #000 var(--candles-fade-left),
            #000 calc(100% - var(--candles-fade-right)),
            transparent 100%);
}

.candlesticks-chart__scroller--more-left {
    --candles-fade-left: var(--s7);
}

.candlesticks-chart__scroller--more-right {
    --candles-fade-right: var(--s7);
}

.candlesticks-chart__scroller::-webkit-scrollbar {
    display: none;
}

.candlesticks-chart__scroller--dragging {
    cursor: grabbing;
}

/*
 * The rule between the candles and a panel below them, and between one panel
 * and the next. Its own element rather than a border on the canvas: with
 * `box-sizing: border-box` set globally, a border or padding on a canvas comes
 * out of the height it was told to be and squashes the bitmap into it.
 */
.candlesticks-chart__divider {
    height: 2px;
    margin: var(--s3) 0;
    background: var(--border);
}

/* -------------------------------------------------- collapsible section */
/*
 * A titled panel that folds away. Built on `<details>`/`<summary>` rather than
 * a div and a click handler, so the open state, keyboard operation and
 * find-in-page all work with no script at all — `/js/components/
 * collapsibleSection.js` only adds the parts markup cannot do (a live count in
 * the header, remembering the state, reacting to a toggle).
 *
 * Not `.collapse`: that is Bootstrap's, both stylesheets are loaded, and a
 * collision here is silent.
 *
 * `overflow: hidden` is what makes the corners round against the summary's
 * hover fill, and it also stops a wide child bleeding past the border.
 */
.collapsible-section {
    margin: var(--s4) var(--s5);
    background: var(--surface-1);
    border: 2px solid var(--border);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-hard);
    overflow: hidden;
}

.collapsible-section__summary {
    /* Overrides Bootstrap's reboot `summary { display: list-item }`, which puts
       a bullet in the row. `list-style` and the webkit marker below remove the
       native triangle in the browsers that draw it differently. */
    display: flex;
    align-items: baseline;
    gap: var(--s3);
    padding: var(--s3) var(--s4);
    cursor: pointer;
    list-style: none;
    user-select: none;
}

.collapsible-section__summary::-webkit-details-marker {
    display: none;
}

/* The disclosure triangle, drawn from borders so it needs no asset and takes
   its colour from a token. */
.collapsible-section__summary::before {
    content: "";
    flex: none;
    align-self: center;
    width: 0;
    height: 0;
    border-left: 6px solid var(--ink-2);
    border-top: 5px solid transparent;
    border-bottom: 5px solid transparent;
    transition: transform .12s;
}

.collapsible-section[open] > .collapsible-section__summary::before {
    transform: rotate(90deg);
}

.collapsible-section__summary:hover {
    background: var(--surface-2);
}

.collapsible-section__summary:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: -3px;
    box-shadow: var(--shadow-glow);
}

.collapsible-section__title {
    color: var(--ink-0);
    font-weight: 600;
}

/*
 * The right-hand note in the header — a count, a timestamp, a status. Pushed
 * over by `margin-left: auto` so it stays on the far edge however long the
 * title is. `--ink-1`, not `--ink-2`: this sits on `--surface-2` while the row
 * is hovered, where `--ink-2` is 4.4:1 and misses the AA floor.
 */
.collapsible-section__meta {
    margin-left: auto;
    color: var(--ink-1);
    font-size: 13px;
}

.collapsible-section__body {
    padding: var(--s4);
    border-top: 2px solid var(--border);
}

/*
 * A body whose child already carries its own padding and gutters — a
 * `.table-scroll`, say. Without this the two paddings compound.
 */
.collapsible-section__body--flush {
    padding: 0;
}

.collapsible-section__body--flush > .table-scroll {
    margin: 0;
}

@media (prefers-reduced-motion: reduce) {
    .collapsible-section__summary::before {
        transition: none;
    }
}

/* --------------------------------------------------------------- tables */

table {
    border-collapse: collapse;
    color: var(--ink-0);
}

/*
 * Wrapper for a table with more columns than a narrow viewport can hold. The
 * table scrolls inside its own box; without it the *page* scrolls sideways,
 * which drags the nav and the headings off-screen along with it.
 */
.table-scroll {
    overflow-x: auto;
    margin: var(--s4) var(--s5);
    /* The default scrollbar is a bright bar across a dark page. */
    scrollbar-width: thin;
    scrollbar-color: var(--border) transparent;
}

/*
 * Undoes bootstrap's `.table { width: 100% }`, which crushes columns to fit the
 * wrapper instead of scrolling it — the symbols column breaks to six lines and
 * the action buttons stack, so a row that should be one line tall becomes six.
 * `max-content` rather than `auto`: a table with `width: auto` still resolves to
 * roughly its *minimum* content width when the space is tight, which is the
 * wrapping we are trying to avoid. `min-width: 100%` keeps a narrow table
 * filling the width rather than shrinking to its content.
 */
.table-scroll > table {
    width: max-content;
    min-width: 100%;
    margin-left: 0;
    margin-right: 0;
}

th {
    color: var(--ink-2);
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .06em;
    text-align: left;
    padding: var(--s2) var(--s3);
    border-bottom: 2px solid var(--border);
}

td {
    padding: var(--s2) var(--s3);
    border-bottom: 1px solid var(--border);
    font-family: var(--font-data);
    font-variant-numeric: tabular-nums;
}

/* Semantic value colors. Reserved — do not reuse for decoration. */
.value-up {
    color: var(--up);
}

.value-down {
    color: var(--down);
}

/* --------------------------------------------------------- entity cards */
/*
 * A record as a card rather than a table row.
 *
 * A nine-column table of UUIDs and symbol lists does not fit a laptop viewport,
 * and both ways out are bad: squeeze the columns and every row becomes six
 * lines tall, or scroll sideways and the actions sit off-screen behind a
 * scrollbar. Cards reflow instead — one per row on a phone, as many as fit on a
 * wide monitor — and each field carries its own label, so nothing depends on a
 * header row that may have scrolled out of view.
 *
 * `.entity-card` and not `.card`: bootstrap is still loaded and owns that name.
 */

.card-grid {
    display: grid;
    /* auto-fill + minmax is the whole responsive story: no breakpoints, the
       column count follows the width it is actually given. */
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: var(--s4);
    margin: var(--s4) var(--s5);
}

.entity-card {
    display: flex;
    flex-direction: column;
    gap: var(--s3);
    padding: var(--s4);
    background: var(--surface-1);
    border: 2px solid var(--border);
    border-radius: var(--radius-card);
}

.entity-card__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s3);
}

.entity-card__title {
    margin: 0;
    font-size: 17px;
    font-weight: 700;
    color: var(--ink-0);
    overflow-wrap: anywhere;
}

.entity-card__id {
    margin: 0;
    font-family: var(--font-data);
    font-size: 11px;
    color: var(--ink-2);
    overflow-wrap: anywhere;
}

/* dt/dd are grid items directly — a two-column label/value list. */
.entity-card__facts {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    gap: var(--s2) var(--s4);
    margin: 0;
    padding: var(--s3) 0;
    border-top: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
}

.entity-card__facts dt {
    color: var(--ink-2);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
}

.entity-card__facts dd {
    margin: 0;
    font-family: var(--font-data);
    font-variant-numeric: tabular-nums;
    overflow-wrap: anywhere;
}

/* Pinned to the bottom edge so a row of cards lines its actions up even when
   one of them carries three more symbols than its neighbour. */
.entity-card__actions {
    margin-top: auto;
}

/* Four controls have to fit a 320px card without wrapping to a second line. */
.entity-card__actions button,
.entity-card__facts button {
    padding: 6px var(--s3);
    font-size: 13px;
}

/*
 * LIVE is the mode with real money behind it, so it gets the accent and
 * SIMULATION stays muted. Driven off the attribute rather than a `chip--on`
 * class in the markup because the template has the mode as a string and
 * mustache cannot compare it to one.
 */
.entity-card[data-mode="LIVE"] {
    border-color: var(--accent);
}

.entity-card[data-mode="LIVE"] .entity-card__mode {
    color: var(--ink-0);
}

.entity-card[data-mode="LIVE"] .entity-card__mode::before {
    background: var(--accent);
    box-shadow: 0 0 8px var(--accent-glow);
}

/* ---------------------------------------------- retro modifiers, opt-in */

.title--bitmap {
    font-family: var(--font-display);
    font-weight: 400;
    letter-spacing: .06em;
    text-transform: uppercase;
    line-height: 1.3;
}

.surface--crt {
    position: relative;
}

.surface--crt::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    border-radius: inherit;
    background: repeating-linear-gradient(
            to bottom,
            var(--scanline) 0,
            var(--scanline) 1px,
            transparent 1px,
            transparent 3px
    );
}

/* --------------------------------------------------------------- modals */
/*
 * Own implementation rather than bootstrap's — `.modal-backdrop-nautilus` and not `.modal-backdrop`
 * precisely so the two cannot collide while bootstrap is still loaded.
 */

.modal-backdrop-nautilus {
    position: fixed;
    inset: 0;
    z-index: 1080;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--s5);
    background: rgba(5, 6, 10, .78);
}

/*
 * A dialog that lives in the page markup rather than being built by JS is closed most of the time,
 * and `hidden` is how it says so. It needs stating here because the UA sheet's `[hidden]` is a bare
 * `display: none` that the class's `display: flex` outranks — the attribute would be set, and the
 * dialog would sit there anyway.
 */
.modal-backdrop-nautilus[hidden] {
    display: none;
}

.modal-nautilus {
    width: min(460px, 100%);
    padding: var(--s5);
    background: var(--surface-1);
    border: 3px solid var(--border);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-hard), 0 0 60px rgba(0, 0, 0, .6);
}

/*
 * For a dialog holding a form rather than a sentence. Wider, and capped at the height the backdrop
 * gives it so a long form scrolls its own body — the actions stay on screen instead of being pushed
 * past the bottom, which on a form is the difference between "long" and "cannot be submitted".
 */
.modal-nautilus--form {
    width: min(860px, 100%);
    max-height: 100%;
    display: flex;
    flex-direction: column;
}

.modal-nautilus__title {
    margin: 0 0 var(--s3);
    font-size: 17px;
    color: var(--ink-0);
}

/* Title on the left, the close button hard right, both on the dialog's top line. */
.modal-nautilus__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--s3);
}

.modal-nautilus__body {
    margin: 0 0 var(--s5);
    color: var(--ink-1);
    font-size: 14px;
}

.modal-nautilus--form .modal-nautilus__body {
    flex: 1 1 auto;
    overflow-y: auto;
}

.modal-nautilus__actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--s2);
}

/* ---------------------------------------------------------------- shell */

.shell-nav {
    display: flex;
    align-items: center;
    gap: var(--s4);
    flex-wrap: wrap;
    padding: var(--s3) var(--s5);
    background: var(--surface-1);
    border-bottom: 2px solid var(--border);
}

.shell-nav__brand {
    display: inline-flex;
    align-items: baseline;
    gap: var(--s2);
    color: var(--ink-0);
    font-family: var(--font-display);
    font-size: 24px;
    letter-spacing: .04em;
    text-transform: uppercase;
}

.shell-nav__brand:hover {
    color: var(--ink-0);
    text-decoration: none;
    text-shadow: 0 0 18px var(--accent-glow);
}

.shell-nav__links {
    display: flex;
    align-items: center;
    gap: var(--s2);
    list-style: none;
    margin: 0;
    padding: 0;
}

.shell-nav__link {
    display: inline-block;
    padding: 6px var(--s3);
    border: 2px solid transparent;
    border-radius: 999px;
    color: var(--ink-1);
    font-weight: 600;
}

.shell-nav__link:hover {
    color: var(--ink-0);
    background: var(--surface-2);
    border-color: var(--border);
    text-decoration: none;
}

.shell-nav__link[aria-current="page"] {
    color: var(--ink-0);
    border-color: var(--accent);
    box-shadow: var(--shadow-glow);
}

.shell-nav__spacer {
    flex: 1 1 auto;
}

.shell-nav__logout {
    background: transparent;
    color: var(--ink-1);
    border-color: var(--border);
}

.shell-nav__logout:hover {
    color: var(--ink-0);
    border-color: var(--ink-2);
    filter: none;
}

.shell-nav__toggle {
    display: none;
    background: transparent;
    border-color: var(--border);
    color: var(--ink-0);
    font-family: var(--font-data);
}

@media (max-width: 720px) {
    .shell-nav__toggle {
        display: inline-block;
        order: 2;
        margin-left: auto;
    }

    .shell-nav__links,
    .shell-nav__logout {
        display: none;
        order: 3;
    }

    .shell-nav--open .shell-nav__links {
        display: flex;
        flex-direction: column;
        align-items: stretch;
        width: 100%;
    }

    .shell-nav--open .shell-nav__logout {
        display: inline-block;
        width: 100%;
    }
}

/* ------------------------------------------------------------ deck plate */
/*
 * The bolted plate at the head of a page: a stamped eyebrow, the title in the
 * display face, an optional lede, and an optional figure bled into the right
 * edge. Rendered by `deckPlate.mustache` — see docs/design/components.md.
 *
 * Hero surfaces only, one per page. It is the loudest thing on the page it
 * heads, which is the whole reason to reach for it and the reason not to put a
 * second one below the first.
 *
 * The home page's `.bridge__hero` is the same language written separately: it
 * seats an animated sonar scope where this seats a figure, and it stayed page-
 * specific rather than becoming a modifier here. If a third variant turns up,
 * that is the point to reconsider — not before.
 */

.deck-plate {
    position: relative;
    padding: var(--s6);
    background:
            radial-gradient(ellipse 70% 130% at 6% 0%, rgba(88, 101, 242, .14), transparent 66%),
            linear-gradient(180deg, var(--surface-1), var(--surface-0));
    border: 3px solid var(--border);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-hard);
    overflow: hidden;
}

/* The hatch's fastenings, same four corners as the bridge's hull plate. */
.deck-plate::before {
    content: "";
    position: absolute;
    inset: 0;
    /* Above the figure, which is positioned as well and would otherwise paint
       over the two right-hand studs by virtue of coming later in the tree. The
       plate's fastenings sit on top of what is bolted under them. */
    z-index: 2;
    pointer-events: none;
    border-radius: inherit;
    background-image:
            radial-gradient(circle at 18px 18px, var(--border) 3.5px, transparent 4.5px),
            radial-gradient(circle at calc(100% - 18px) 18px, var(--border) 3.5px, transparent 4.5px),
            radial-gradient(circle at 18px calc(100% - 18px), var(--border) 3.5px, transparent 4.5px),
            radial-gradient(circle at calc(100% - 18px) calc(100% - 18px), var(--border) 3.5px, transparent 4.5px);
}

.deck-plate__text {
    position: relative;
    z-index: 1;
    max-width: 62ch;
}

/*
 * Stamped, typed in sentence case and uppercased by CSS — see
 * docs/design/copy.md. --ink-1 rather than --ink-2: the accent wash is centred
 * on this corner, where --ink-2 drops under the AA floor.
 */
.deck-plate__eyebrow {
    margin: 0 0 var(--s3);
    font-family: var(--font-data);
    font-size: 11px;
    letter-spacing: .32em;
    text-transform: uppercase;
    color: var(--ink-1);
}

/* Silkscreen's floor is 24px — see docs/design/typography.md. */
.deck-plate__title {
    margin: 0;
    font-size: clamp(28px, 5vw, 46px);
    color: var(--ink-0);
}

.deck-plate__lede {
    margin: var(--s4) 0 0;
    color: var(--ink-1);
    font-size: 15px;
}

/*
 * The figure, off to the right of the plate.
 *
 * Out of flow and sized by `height`, so the plate's height is still set by the
 * words alone and the figure fits whatever that comes to — the alternative, an
 * in-flow column, grew the head of the page by half again to seat a decoration.
 *
 * `top: 0` is the padding box's top edge, so the top of the art is never
 * cropped, and `right: 0` is the inner edge of the border, so the figure bleeds
 * through the plate's own padding to sit flush against it. The overhang past
 * 100% carries a bust's existing cut at the chest off the bottom edge — the
 * plate's `overflow: hidden` does the clipping — rather than adding a second,
 * squarer cut of its own. Art that is *not* cut off at its own bottom edge will
 * look sheared here; crop it before using it.
 *
 * Left unstyled by `image-rendering`: the portraits ship at 480px against a
 * ~215px slot, so this is a downscale, and `pixelated` — nearest-neighbour —
 * would drop rows of a sprite and tear its diagonals. The smooth default keeps
 * pixel art's blocks readable as blocks.
 */
.deck-plate__figure {
    display: none;
    position: absolute;
    top: 0;
    right: 0;
    height: calc(100% + var(--s5));
    width: auto;
    /* Held back so the figure reads as part of the plate and the title stays
       the loudest thing on it. */
    opacity: .55;
}

/* Wide enough to seat the figure beside the words without crowding them — the
   same breakpoint the bridge uses to seat its scope. The padding is what keeps
   the two apart: the figure is out of flow and cannot reserve its own room, and
   it is the wider guard of the two, holding at plate heights the ledes in the
   app do not produce today. It rides on the modifier rather than on
   `.deck-plate` itself so a plate with no figure does not reserve a column for
   one. */
@media (min-width: 900px) {
    .deck-plate--figured {
        padding-right: 280px;
    }

    .deck-plate--figured .deck-plate__figure {
        display: block;
    }
}

/* ------------------------------------------------------- login: the hatch */
/*
 * The login page is the pressure hatch of the Nautilus: a bolted steel rim,
 * a dogging wheel behind it, and lit glass in the middle holding the form.
 * Page-specific for now; it moves to its own file when there is a build step.
 */

.hatch-page {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--s5);
    min-height: 100vh;
    padding: var(--s6) var(--s4);
    background:
            radial-gradient(ellipse 80% 55% at 50% -10%, rgba(88, 101, 242, .16), transparent 70%),
            radial-gradient(ellipse 60% 50% at 50% 115%, rgba(87, 242, 135, .06), transparent 70%),
            var(--surface-0);
}

/*
 * Sized for its contents, which is awkward for a circle: the usable width at
 * the top and bottom of the glass is a chord, not the diameter, so a column
 * 260px wide only has ~2*sqrt(r^2 - 130^2) of height to live in. Adding the
 * Google button pushed past that at 600px and the button was clipped.
 *
 * Size for the *tallest* state, which is brand + error + form + Google button,
 * not the empty page — an error message that only appears on failure is
 * exactly the content nobody checks, and it lands when the user is already
 * having a bad time.
 *
 * Widening the glass instead is not available — its 18% inset puts its edge at
 * 32% of the hatch, just inside the inner lip the ring SVG draws at r=134/400
 * (33.5%). Grow the glass and it covers the lip. So the hatch grows instead,
 * and the rhythm inside it is tighter than the rest of the system's.
 *
 * The diameter is published as --hatch-d so everything inside the glass can be
 * a fraction of it instead of a fixed pixel size. That is what lets the circle
 * survive a short viewport: fixed content in a 80vh circle could only ever fit
 * at ~710px across, which put the fallback threshold at 900px of viewport
 * height — i.e. every windowed *and* most full-screen laptop browsers. The
 * ratios below are all "value at 720 / 720", so the full-size hatch is
 * unchanged to the pixel; the clamp floors are where legibility stops it.
 */
.hatch {
    --hatch-d: min(720px, 92vw, 88vh);

    position: relative;
    width: var(--hatch-d);
    aspect-ratio: 1;
    flex: none;
}

.hatch__ring {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    filter: drop-shadow(0 0 40px rgba(0, 0, 0, .8));
}

.hatch__wheel {
    transform-box: fill-box;
    transform-origin: center;
}

.hatch--sealing .hatch__wheel {
    animation: hatch-seal .6s steps(8) forwards;
}

@keyframes hatch-seal {
    to {
        transform: rotate(75deg);
    }
}

/* The lit glass in the middle, and everything the user actually reads. */
.hatch__glass {
    position: absolute;
    inset: 18%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: clamp(2px, calc(var(--hatch-d) * 4 / 720), 4px);
    padding: clamp(9px, calc(var(--hatch-d) * 16 / 720), 16px);
    border-radius: 50%;
    background:
            radial-gradient(circle at 50% 22%, rgba(88, 101, 242, .30), transparent 62%),
            radial-gradient(circle at 50% 55%, #0D0F18 0%, #05060A 80%);
    box-shadow:
            inset 0 0 0 3px #454C63,
            inset 0 2px 14px rgba(160, 175, 220, .18),
            inset 0 0 70px rgba(0, 0, 0, .95),
            0 0 70px var(--accent-glow);
    overflow: hidden;
    text-align: center;
}

.hatch__brand {
    margin: 0;
    font-size: clamp(20px, calc(var(--hatch-d) * 30 / 720), 30px);
    color: var(--ink-0);
    text-shadow: 0 0 20px var(--accent-glow);
}

.hatch__stamp {
    margin: 0;
    font-family: var(--font-data);
    font-size: 11px;
    letter-spacing: .22em;
    text-transform: uppercase;
    color: var(--ink-2);
}

/*
 * Sits between the brand and the form, so it is read before the fields are
 * refilled rather than after. Two lines at 260px is the budget — the glass is a
 * circle and a third line eats the clearance the Google button needs.
 */
.hatch__error {
    width: clamp(210px, calc(var(--hatch-d) * 260 / 720), 260px);
    margin: 0;
    color: var(--down);
    font-family: var(--font-data);
    font-size: clamp(11px, calc(var(--hatch-d) * 12 / 720), 12px);
    line-height: 1.35;
}

.hatch__form {
    display: flex;
    flex-direction: column;
    gap: clamp(5px, calc(var(--hatch-d) * 8 / 720), 8px);
    width: clamp(210px, calc(var(--hatch-d) * 260 / 720), 260px);
    margin-top: var(--s1);
}

.hatch__field {
    display: flex;
    flex-direction: column;
    gap: 4px;
    text-align: left;
}

.hatch__field label {
    font-family: var(--font-data);
    font-size: clamp(9px, calc(var(--hatch-d) * 10 / 720), 10px);
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--ink-2);
}

.hatch__field input {
    width: 100%;
    background: rgba(11, 11, 16, .75);
    border-color: #3A3F52;
    font-size: clamp(12px, calc(var(--hatch-d) * 14 / 720), 14px);
    padding: clamp(5px, calc(var(--hatch-d) * 7 / 720), 7px) var(--s3);
}

.hatch__field input:focus-visible {
    border-color: var(--accent);
    outline-offset: 1px;
}

.hatch__submit {
    margin-top: var(--s1);
    width: 100%;
    padding: clamp(6px, calc(var(--hatch-d) * 9 / 720), 9px) var(--s4);
    font-size: clamp(13px, calc(var(--hatch-d) * 15 / 720), 15px);
    letter-spacing: .04em;
}

/*
 * The second way in. Sits under the form inside the same glass, separated by a
 * rule rather than given its own panel — it is another hatch, not another page.
 */
.hatch__alt {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s1);
    width: clamp(210px, calc(var(--hatch-d) * 260 / 720), 260px);
    margin-top: clamp(5px, calc(var(--hatch-d) * 8 / 720), 8px);
}

.hatch__alt-rule {
    display: flex;
    align-items: center;
    gap: var(--s2);
    width: 100%;
    font-family: var(--font-data);
    font-size: clamp(9px, calc(var(--hatch-d) * 10 / 720), 10px);
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--ink-2);
}

.hatch__alt-rule::before,
.hatch__alt-rule::after {
    content: "";
    flex: 1;
    height: 1px;
    background: linear-gradient(90deg, transparent, #3A3F52, transparent);
}

.hatch__google {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--s2);
    width: 100%;
    padding: clamp(5px, calc(var(--hatch-d) * 7 / 720), 7px) var(--s3);
    border: 1px solid #3A3F52;
    border-radius: var(--radius-ctl);
    background: rgba(11, 11, 16, .75);
    color: var(--ink-0);
    font-size: clamp(11.5px, calc(var(--hatch-d) * 13 / 720), 13px);
    text-decoration: none;
    transition: border-color .15s ease, background .15s ease;
}

.hatch__google:hover,
.hatch__google:focus-visible {
    border-color: var(--accent);
    background: rgba(24, 26, 38, .9);
    text-decoration: none;
}

.hatch__google-mark {
    width: 16px;
    height: 16px;
    flex: none;
}

.hatch__caption {
    margin: 0;
    font-family: var(--font-data);
    font-size: 12px;
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--ink-2);
}

/*
 * Below a certain diameter a circle cannot hold a form without squeezing the
 * inputs to nothing, so the hatch becomes a pixel-stepped viewport instead.
 * Same language, different port.
 *
 * Both thresholds are the *same* number reached from two sides: the smallest
 * hatch the glass still works in, which is 670px across. Measured against the
 * tallest state — brand + a two-line error + form + Google button — the worst
 * corner clears the glass edge by ~9px at 720px of hatch, ~7px at 670, ~3px at
 * 620, and nothing at all by 600, because the type clamps bottom out (input
 * 12px, labels 9px) and the content stops shrinking while the circle keeps
 * going. 670 is the last size with a margin comparable to the full-size hatch,
 * and it leaves room for an error that wraps to a third line — that costs ~14px
 * of clearance and goes negative almost immediately, which is why LoginError
 * keeps its wording short. Since the diameter is min(720px, 92vw, 88vh), 670px
 * of hatch needs 728px of width (670 / .92) and 761px of height (670 / .88).
 *
 * The height figure used to be 900px, back when the glass content was fixed
 * pixels: fixed content only fits at ~710px of hatch, which needs 887px of
 * viewport. That put the cliff directly on top of a full-screen laptop browser
 * — 1512x982 leaves ~904px of viewport bare and ~874px with a bookmarks bar,
 * so the login page changed shape depending on whether a toolbar was showing.
 * Scaling the interior from --hatch-d is what moved the cliff out of the way.
 *
 * Any full-screen or reasonably sized window now gets the hatch. A 720p screen,
 * a phone, or a genuinely small window gets the port.
 */
@media (max-width: 730px), (max-height: 760px) {
    .hatch {
        /*
         * The port is a rectangle with room to spare, so the interior goes back
         * to its full-size values rather than inheriting a diameter it no
         * longer has.
         */
        --hatch-d: 720px;

        width: 100%;
        max-width: 380px;
        aspect-ratio: auto;
    }

    .hatch__ring {
        display: none;
    }

    .hatch__glass {
        position: static;
        inset: auto;
        border-radius: var(--radius-card);
        padding: var(--s6) var(--s5);
        border: 3px solid #3A3F52;
        box-shadow: var(--shadow-hard), 0 0 50px var(--accent-glow);
    }

    .hatch__form,
    .hatch__alt {
        width: 100%;
    }
}

@media (prefers-reduced-motion: reduce) {
    .hatch--sealing .hatch__wheel {
        animation: none;
    }

    * {
        transition: none !important;
    }
}

/* --------------------------------------------- bootstrap 4 compat shims */
/*
 * Temporary. Bootstrap still ships for the ~30 un-migrated pages and its
 * class selectors outrank the element styles above. Delete this whole block
 * together with the bootstrap <link>.
 */

.table,
.table th,
.table td {
    color: var(--ink-0);
    border-color: var(--border);
}

.table-striped tbody tr:nth-of-type(odd) {
    background-color: var(--surface-1);
}

.card,
.modal-content,
.list-group-item {
    background-color: var(--surface-1);
    color: var(--ink-0);
    border-color: var(--border);
}

.modal-header,
.modal-footer {
    border-color: var(--border);
}

.close {
    color: var(--ink-0);
    text-shadow: none;
}

.form-control {
    background-color: var(--surface-2);
    color: var(--ink-0);
    border-color: var(--border);
}

.form-control:focus {
    background-color: var(--surface-2);
    color: var(--ink-0);
    border-color: var(--accent);
    box-shadow: var(--shadow-glow);
}
