/*
 * Load Test Mode: what is specific to this page.
 *
 * The reset, the palette, the type scale and the components both app pages share live in
 * css/tokens.css and css/common.css, which load before this sheet. What is left here is this
 * page's own furniture -- the toolbar, the tabs, the side panels, the bottom bar. The log panel
 * and the log tables are the one part of it large enough to read on their own, and they are in
 * css/log.css.
 *
 * This used to be a <style> block inside index.html, which is why every rule below still explains
 * itself at length: the page was 2884 lines of CSS, markup and script in one file and the prose
 * was the only thing telling them apart.
 *
 * It once declared a "unified theme" layer 300 lines below the components that re-declared the
 * background of .panel, .toolbar-section, .tab.active and .protocol-options-toolbar, so editing a
 * component did nothing. Each surface is now decided once, where it is declared.
 */

/*
 * The frame. The header and the footer are fixed bands at the top and the bottom of the
 * window and .app-body is everything in between, which is the element that scrolls -- see
 * its rule below. Full width on purpose: the two bands are full-bleed gradients, so
 * capping the container at 1600px left 480px of empty page beside them on a 2560px screen
 * and made the whole application look like a card someone forgot to centre. A device
 * table and a log table are also the two things on this page that a wide screen actually
 * helps.
 *
 * Below 768px this stops being a frame at all; see the last media query.
 */
.app-container {
    display: flex;
    flex-direction: column;
    height: 100vh;
    padding: 0;
    gap: 0;
}

/* Header */
.app-header-left {
    display: flex;
    align-items: center;
    gap: var(--sp-4);
}
.app-logo {
    width: 32px;
    height: 32px;
    background: rgba(255,255,255,0.2);
    border-radius: var(--r-md);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--fs-500);
}
.app-title {
    font-size: var(--fs-500);
    font-weight: 600;
    letter-spacing: -0.5px;
}
.app-subtitle {
    font-size: var(--fs-100);
    opacity: 0.8;
    margin-top: 1px;
}
/* Not the same object as .header-status on the Single page, which is one inline run of
   indicators; here it is the whole right-hand group. At 400px width nowrap ran the connection
   label, the version badge, API Docs and User Guide past the right edge with nothing to scroll
   them into view -- flex-wrap costs nothing at the widths where the row already fits. */
.header-status {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--sp-6);
}
.header-status-item {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    font-size: var(--fs-200);
    opacity: 0.9;
}
.connection-dot {
    width: var(--sp-3);
    height: var(--sp-3);
    border-radius: 50%;
    background: var(--success);
    animation: pulse 2s infinite;
}
.version-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    font-family: var(--font-mono);
    font-size: var(--fs-100);
    font-weight: 500;
    color: rgba(255, 255, 255, 0.85);
    background: rgba(255, 255, 255, 0.12);
    padding: 3px var(--sp-3);
    border-radius: var(--r-sm);
    border: 1px solid rgba(255, 255, 255, 0.15);
    cursor: help;
    white-space: nowrap;
}
.version-badge .commit {
    opacity: 0.65;
    font-size: var(--fs-100);
}

/*
 * Everything between the two bands, and the element that owns the vertical scroll on a
 * desktop width. It used to be overflow: hidden, which on a short or narrow window did not
 * hide anything the user could get back -- it cut it off. Every band stacked in here keeps
 * its own height (flex: 0 0 auto below) and .main-content keeps a floor, so when the
 * window is too short the column overflows this box and this box scrolls, instead of the
 * bands being squeezed under each other.
 *
 * overflow-y also keeps the automatic minimum size of this flex item at zero, which is
 * what stops a tall column from pushing the footer off the bottom of the window.
 */
.app-body {
    flex: 1;
    display: flex;
    flex-direction: column;
    padding: var(--sp-4);
    gap: var(--sp-3);
    overflow-y: auto;
}

/* Top toolbar - like desktop */
.toolbar {
    display: flex;
    gap: var(--sp-3);
    /* The Status section, with the device count and the Insecure badge, is the last thing
       on this row. With nowrap it left the window below about 1420px and nothing scrolled
       sideways to reach it -- and that badge is the only way into the transport security
       settings. */
    flex-wrap: wrap;
    row-gap: var(--sp-3);
    flex: 0 0 auto;
    background: var(--surface-0);
    padding: var(--sp-4) var(--sp-5);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-1);
    align-items: stretch;
    border: 1px solid var(--border);
}
.toolbar-section {
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    padding: var(--sp-3) var(--sp-4);
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    background: var(--surface-2);
}
/* display is stated because the Protocol section's title is the <label> of the select under it
   and the other four are <div>s. A label is inline by default, and the margin below would then
   have pushed nothing down. */
.toolbar-section-title {
    display: block;
    font-size: var(--fs-100);
    font-weight: 600;
    color: var(--text-2);
    margin-bottom: var(--sp-2);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.toolbar-row {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
}
.toolbar-row label {
    font-size: var(--fs-200);
    color: var(--text-2);
    min-width: fit-content;
}
.toolbar-row input, .toolbar-row select {
    padding: var(--sp-1) var(--sp-3);
    font-size: var(--fs-200);
}
.toolbar-row input[type="text"], .toolbar-row input[type="number"] {
    width: 85px;
}
.toolbar-row select { min-width: 90px; }

/*
 * The live Groups x Concurrent total beside the CPE Amount title (js/load/config.js). A reading, not
 * a heading, so it drops the title's capitals and weight -- except as a warning, which also says
 * "a large run" in words for anyone who cannot tell the colour apart.
 */
.device-total {
    margin-left: var(--sp-3);
    font-weight: 400;
    text-transform: none;
    color: var(--text-2);
}
.device-total.is-warning {
    font-weight: 600;
    color: var(--warning-fg);
}
/*
 * The title that carries the total takes up exactly the room it had before anything was typed. The
 * total's length follows the fields, and a title sized by it moved the page as the user typed: a
 * ten-digit total took the section from 360px to 409px wide at a 768px window, shifting the
 * sections beside it, and letting it wrap instead made a 1,000,000-device total grow the toolbar
 * by 13px at 1024px. So the title adds nothing to the section's width -- the fields below decide
 * it -- stays one line, and cuts a total too long for the section with an ellipsis. The number
 * comes first, so what is cut is the trailing words; the output's text is whole for a screen
 * reader, and the warning is also its colour, its weight and its tooltip. No floor from the
 * responsive layout is changed.
 */
.cpe-amount-title {
    contain: inline-size;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Editable combobox (Server IP/Port) — matches the desktop editable ComboBox */
.combo { position: relative; display: inline-flex; align-items: center; }
.toolbar-row .combo > input { width: 100%; padding-right: 22px; box-sizing: border-box; }
.combo-caret {
    position: absolute; right: 1px; top: 1px; bottom: 1px; width: 18px;
    border: none; border-left: 1px solid var(--border); background: var(--surface-1);
    color: var(--text-2); font-size: var(--fs-100); line-height: 1; cursor: pointer;
    display: flex; align-items: center; justify-content: center; border-radius: 0 3px 3px 0;
}
.combo-caret:hover { background: var(--surface-3); }
.combo-list {
    position: absolute; top: calc(100% + 2px); left: 0; z-index: 300;
    margin: 0; padding: 0; list-style: none; min-width: 100%; width: max-content; max-width: 280px;
    background: var(--surface-0); border: 1px solid var(--border); border-radius: var(--r-sm);
    box-shadow: var(--shadow-2); max-height: 190px; overflow-y: auto; display: none;
}
.combo.open .combo-list { display: block; }
.combo-item { padding: var(--sp-1) var(--sp-3); font-size: var(--fs-200); cursor: pointer; white-space: nowrap; color: var(--text-1); }
.combo-item:hover { background: var(--surface-1); }

/* Status wraps onto the next toolbar row like every other section: it used to be the one
   section forbidden to shrink, so it was pushed out of the window rather than down. */
/*
 * Status takes the rest of the row: the room the sections before it leave used to sit empty at the
 * end of the toolbar. It still shrinks and wraps like the rest. What fills it is css/load-status.css.
 */
.toolbar-section.status-section {
    flex: 1 1 auto;
    min-width: 160px;
    background: var(--surface-1);
    border-color: var(--border);
}

/*
 * Main content area -- and the floor for the whole middle of the page.
 *
 * The floor used to sit three levels down, on the panel inside .center-panel, while this
 * element was free to shrink to nothing. .center-panel has no overflow of its own, so on a
 * short window the panel simply drew outside its parent and over the Config Files row and
 * the log panel below it. The floor belongs here, on the item the column hands height to:
 * once there is less room than this, .app-body scrolls. Zero would be no better than the
 * spill -- the panels in here clip their own content, so a collapsed middle is a middle
 * nobody can read.
 */
.main-content {
    display: flex;
    gap: var(--sp-3);
    flex: 1 1 auto;
    min-height: 260px;
}

/* Left panel - configuration. It scrolls rather than clips; css/param-tools.css says why. */
.left-panel {
    width: 320px;
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
    flex-shrink: 0;
    overflow-x: hidden;
    overflow-y: auto;
    min-height: 0;
}
/* First panel (Device / Transport Security / Protocol Options tabs) takes all the available vertical
   space and scrolls its tab-content internally, so the panel under it keeps its place however tall
   the tab gets. Its floor: free to shrink, it was 2px tall with that panel open. */
.left-panel > .panel:first-child {
    flex: 1 1 auto;
    min-height: 180px;
    display: flex;
    flex-direction: column;
}
.left-panel > .panel:first-child .tab-content.active {
    flex: 1 1 auto;
    overflow-y: auto;
    min-height: 0;
}
/* Whatever sits under the tabs -- today the Extract panel -- stays a natural-height block. */
.left-panel > .panel:not(:first-child) {
    flex: 0 0 auto;
}

/* Center panel - device tree / table.

   No min-height here on purpose. Beside the other two panels this is a cross-axis size,
   so it stretches to .main-content and its floor; writing a floor of its own is what used
   to make it spill. Stacked below 1024px it becomes a main-axis size, where flex: 1 means
   a zero base and a declared zero would collapse the device table to nothing -- so the
   floor for that layout is declared in the breakpoint that creates it. */
.center-panel {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-width: 0;
}

/* Active Devices and the log stay white; every other panel is the grey chrome from
   css/common.css. A scoped exception, not a second opinion about .panel. */
.center-panel > .panel,
.center-panel > .panel > .panel-header,
.log-panel,
.log-panel > .panel-header { background: var(--surface-0); }

/* Tabs */
.tabs {
    display: flex;
    border-bottom: 1px solid var(--border);
    background: var(--surface-1);
}
/* Negative offset, like .panel-header in css/common.css: the tab strip sits flush in the top-left
   corner of a .panel that clips, so the first tab's ring loses two of its four sides drawn
   outside. */
.tab {
    padding: var(--sp-3) 14px;
    cursor: pointer;
    border: none;
    background: none;
    outline-offset: -2px;
    font-size: var(--fs-200);
    font-weight: 500;
    color: var(--text-2);
    border-bottom: 2px solid transparent;
    transition: color var(--dur-fast), background var(--dur-fast);
}
.tab:hover { color: var(--brand-800); background: var(--brand-050); }
.tab.active {
    color: var(--brand-800);
    border-bottom-color: var(--brand-800);
    background: var(--surface-2);
}
.tab-content { display: none; padding: var(--sp-4); }
.tab-content.active { display: block; }

/* Form elements */
.form-grid {
    display: grid;
    gap: var(--sp-3);
}
/* Not the same object as .form-row on the Single page, which is a two-column grid. */
.form-row {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
}
.form-row label {
    font-size: var(--fs-200);
    color: var(--text-2);
    min-width: 100px;
}
.form-row input, .form-row select {
    flex: 1;
    /* A field's automatic minimum size is its intrinsic width, and fields now render in
       the page font rather than the browser's, which made that intrinsic width wider than
       the column and pushed "Client ID (endpoint):" onto a second line. */
    min-width: 0;
    padding: var(--sp-2) var(--sp-3);
    font-size: var(--fs-200);
    transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
}
/* The soft blue halo is the hover-and-type affordance; it is not a focus indicator. It reads
   1.2:1 against the field beside it, and it used to be paired with outline: none, which put a
   higher-specificity rule in front of the *:focus-visible ring in css/tokens.css and left a
   keyboard user with no way to see where they were. The halo stays, the ring comes back on top
   of it, and only a keyboard focus draws the ring. */
.form-row input:focus, .form-row select:focus {
    box-shadow: 0 0 0 2px var(--brand-100);
}

/* The security pill is a control on this page: it opens the transport settings. */
.security-badge {
    margin-left: var(--sp-3);
    cursor: pointer;
    transition: filter var(--dur-fast), transform var(--dur-fast);
}
.security-badge:hover { filter: brightness(0.95); transform: translateY(-1px); }

.table-container {
    flex: 1;
    overflow: auto;
}

/* Protocol options — compact multi-column grid (denser, closer to desktop) */
/* No own scroll: the parent .tab-content.active already scrolls the tab,
   so a nested max-height/overflow here would produce a second scrollbar. */
.protocol-options {
    min-height: 0;
}
.protocol-options-toolbar {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    /* Over .tab-content's own padding, padded back by the same token; top is negative by it too,
       because sticky holds the MARGIN box: at top: 0 rows showed in the strip above the bar. */
    margin: calc(var(--sp-4) * -1) calc(var(--sp-4) * -1) var(--sp-3);
    padding: var(--sp-4) var(--sp-4) var(--sp-2);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: calc(var(--sp-4) * -1);
    background: var(--surface-2);
    z-index: 1;
}
.protocol-options-toolbar .po-count {
    font-size: var(--fs-100);
    color: var(--text-3);
    white-space: nowrap;
}
.protocol-options-toolbar input {
    flex: 1;
    min-width: 0;
    padding: var(--sp-1) var(--sp-3);
    font-size: var(--fs-200);
}
.protocol-options-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 1px 18px;
}
.protocol-option {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: 3px 0;
    border-bottom: 1px solid var(--surface-3);
    min-width: 0;
}
.protocol-option.po-hidden { display: none; }
.protocol-options-hint {
    font-size: var(--fs-100);
    color: var(--text-3);
    padding: var(--sp-1) 2px var(--sp-3);
    line-height: var(--lh-base);
}
.protocol-option label {
    font-size: var(--fs-200);
    color: var(--text-2);
    min-width: 104px;
    max-width: 104px;
    flex-shrink: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.protocol-option input, .protocol-option select {
    flex: 1;
    min-width: 0;
    padding: 3px var(--sp-2);
    font-size: var(--fs-200);
}
.protocol-option input[type="checkbox"] {
    flex: none;
    width: var(--sp-6);
    height: var(--sp-6);
}
.protocol-option input[type="text"] {
    max-width: none;
}

/* Bottom actions bar — grey band with white input fields (desktop-like) */
/* Like .controls-bar and .toolbar: a band that keeps its own height. Left free to shrink,
   it was squeezed below its content on a short window and the panel above painted over it. */
.bottom-actions-bar { background: var(--surface-2); flex: 0 0 auto; }
.bottom-actions-bar .panel-body { padding: var(--sp-3) var(--sp-5); }
.bottom-actions-bar .config-files label { font-size: var(--fs-200); color: var(--text-2); font-weight: 600; }
.bottom-actions-bar .config-files select,
.bottom-actions-bar input[type="file"] {
    background: var(--surface-0);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    padding: var(--sp-1) var(--sp-2);
    font-size: var(--fs-200);
}

/* Security section */
.security-mode {
    display: flex;
    gap: var(--sp-5);
    margin-bottom: var(--sp-4);
}
.security-mode label {
    display: flex;
    align-items: center;
    gap: var(--sp-1);
    font-size: var(--fs-200);
    cursor: pointer;
}
/* Not the same object as .security-fields on the Single page, which is a styled block;
   here it is a show/hide wrapper. */
.security-fields { display: none; }
.security-fields.active { display: block; }

/* Controls bar */
.controls-bar {
    flex: 0 0 auto;
    background: var(--surface-0);
    padding: var(--sp-4) 14px;
    border-radius: var(--r-md);
    box-shadow: var(--shadow-1);
    border: 1px solid var(--border);
}
.controls-bar .btn-group {
    display: flex;
    gap: var(--sp-3);
    flex-wrap: wrap;
    align-items: center;
}
.controls-bar .btn-group::after {
    content: '';
    flex: 1;
}
.controls-bar .btn {
    padding: var(--sp-3) var(--sp-6);
    font-size: var(--fs-300);
}
.controls-divider {
    width: 1px;
    height: 28px;
    background: var(--border);
    margin: 0 var(--sp-2);
}

/* Bottom bar / Footer. Four runtime toggles plus the status text and, on the right, the build
   badge: at 400px this stayed one nowrap row on both levels and ran the badge past the edge with
   nothing to scroll it into view -- the same defect the header had, in the band below instead of
   above. row-gap matches .toolbar's: wrapped rows need a gap of their own or they touch. */
.bottom-bar {
    display: flex;
    flex-wrap: wrap;
    row-gap: var(--sp-2);
    justify-content: space-between;
    align-items: center;
    background: linear-gradient(to right, var(--brand-800), var(--brand-700));
    padding: var(--sp-3) var(--sp-6);
    color: var(--text-inverse);
}
.bottom-bar-left {
    display: flex;
    flex-wrap: wrap;
    row-gap: var(--sp-2);
    align-items: center;
    gap: var(--sp-8);
}
.status-text {
    font-size: var(--fs-100);
    color: rgba(255,255,255,0.9);
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}
/* The status dot, an 8px circle. Sized as a box: a spacing step is not a type size. */
.status-text .icon {
    width: var(--sp-3);
    height: var(--sp-3);
}
.bottom-bar .checkbox-inline {
    color: rgba(255,255,255,0.8);
    font-size: var(--fs-100);
}
.bottom-bar .checkbox-inline input[type="checkbox"] {
    accent-color: var(--success);
}
.bottom-bar select {
    background: rgba(255,255,255,0.15);
    border: 1px solid rgba(255,255,255,0.3);
    color: var(--text-inverse);
    padding: 3px var(--sp-3);
    border-radius: var(--r-sm);
    font-size: var(--fs-100);
}
.bottom-bar select option {
    background: var(--brand-800);
    color: var(--text-inverse);
}
.footer-brand {
    font-size: var(--fs-100);
    opacity: 0.8;
}

/* Tooltips */
[data-tooltip] {
    position: relative;
}
[data-tooltip]:hover::after {
    content: attr(data-tooltip);
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
    padding: var(--sp-2) var(--sp-4);
    background: var(--text-1);
    color: var(--text-inverse);
    font-size: var(--fs-100);
    border-radius: var(--r-sm);
    white-space: nowrap;
    z-index: 100;
    margin-bottom: var(--sp-1);
}

/* No devices placeholder */
.no-devices {
    text-align: center;
    padding: 40px 30px;
    color: var(--text-2);
}
.no-devices-icon {
    width: 64px;
    height: 64px;
    background: linear-gradient(135deg, var(--surface-1) 0%, var(--surface-3) 100%);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 0 auto var(--sp-5);
}
.no-devices-icon .icon {
    font-size: var(--fs-600);
    color: var(--text-3);
}
.no-devices h3 {
    font-size: var(--fs-400);
    font-weight: 600;
    color: var(--text-1);
    margin-bottom: var(--sp-2);
}
.no-devices p {
    font-size: var(--fs-200);
    color: var(--text-2);
    max-width: 280px;
    margin: 0 auto;
    line-height: var(--lh-base);
}
.no-devices .hint {
    margin-top: var(--sp-6);
    padding: var(--sp-4) 14px;
    background: var(--brand-050);
    border-radius: var(--r-md);
    border: 1px solid var(--brand-100);
    color: var(--brand-800);
    font-size: var(--fs-100);
    display: inline-block;
}
.no-devices .hint .icon {
    margin-right: var(--sp-2);
    color: var(--info-fg);
}

/* Config files section */
.config-files {
    font-size: var(--fs-200);
}
.config-files select {
    width: 100%;
    padding: var(--sp-1);
    margin-bottom: var(--sp-3);
    font-size: var(--fs-200);
}

/* Checkbox inline */
.checkbox-inline {
    display: flex;
    align-items: center;
    gap: var(--sp-1);
    font-size: var(--fs-200);
    cursor: pointer;
}
.checkbox-inline input[type="checkbox"] {
    width: 14px;
    height: 14px;
    accent-color: var(--brand-800);
    cursor: pointer;
}
.checkbox-inline label {
    cursor: pointer;
}

/* Right panel */
.right-panel {
    width: 380px;
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
    flex-shrink: 0;
    overflow: hidden;
    /* Same as .center-panel: no floor of its own next to the others, and one declared by
       the stacking breakpoint, where its flex: 1 child would otherwise size to nothing. */
}
.panel-collapsible .panel-header {
    cursor: pointer;
    user-select: none;
}
.panel-collapsible .panel-header .collapse-caret {
    margin-left: auto;
    color: var(--text-2);
    transition: transform var(--dur-fast) ease;
}
.panel-collapsible.collapsed .panel-body {
    display: none;
}
.panel-collapsible.collapsed .collapse-caret {
    transform: rotate(-90deg);
}
.panel-collapsible.collapsed {
    max-height: none !important;
    flex: 0 0 auto !important;
}
/* Update-param panel body fills available vertical space */
.param-panel-body {
    display: flex;
    flex-direction: column;
    min-height: 0;
    flex: 1;
    padding: var(--sp-4);
    overflow: auto;
}
.grow-textarea {
    flex: 1;
    min-height: 80px;
    font-family: inherit;
    font-size: var(--fs-200);
    padding: var(--sp-2);
    resize: vertical;
}

/*
 * Responsive.
 *
 * Down to 769px the page stays a frame: the header and the footer are fixed to the top and
 * the bottom of the window and .app-body scrolls between them. The three side panels give
 * up width first and then stack, and the toolbar wraps by itself -- no breakpoint decides
 * where, the content does.
 */
@media (max-width: 1400px) {
    .right-panel { width: 340px; }
}
@media (max-width: 1200px) {
    .right-panel { width: 300px; }
    .left-panel { width: 280px; }
}
/*
 * Stacked. Both of the panels that used to fill the row -- the devices panel and the
 * update-param panel -- are flex: 1 with a zero base, which in a row is a share of the
 * width and in a column is a hypothetical height of zero. Without a floor here the column
 * that contains them reports its own height as if they were not in it, and on a window
 * with no room to spare the device table renders 0px tall: not truncated, gone. The floor
 * matches the one .main-content carries above, so the two layouts agree on how much room
 * the middle of the page is worth; .main-content grows to hold it and .app-body scrolls.
 */
@media (max-width: 1024px) {
    /* Stacked, this column contains the panels rather than distributing height to them,
       so it must not be shrunk below them: side by side its children stretch to whatever
       height it ends up with, stacked they add up to more than it and spill over the bars
       below. Measured at 1024x768: 203px of devices panel drawn across the Config Files
       row. It still grows into spare height; it just no longer gives any back. */
    .main-content { flex-direction: column; flex-shrink: 0; }
    .left-panel, .right-panel { width: 100%; }
    .center-panel { min-height: 260px; }
    .right-panel { min-height: 200px; }
}
/*
 * Below this the frame is the problem rather than the layout. Three stacked panels, a
 * toolbar in several rows and a log table do not fit between a fixed header and a fixed
 * footer at any useful size, so the page stops pretending: the container grows with its
 * content and .app-body hands the scroll back to the document. One scrollbar, the
 * window's, instead of one inside another.
 *
 * What this does not do is put every panel at its natural height. The floors the 1024
 * breakpoint gives .center-panel and .right-panel still apply here, and a floor is a
 * minimum rather than a size: each of those two panels ends up as tall as its content or
 * its floor, whichever is the larger. Measured at 760x800, the devices panel is 260px --
 * its floor, with room to spare inside -- when the table holds one device, 322px with the
 * empty state, and 1655px with forty devices. In all three the document does the
 * scrolling and nothing scrolls inside the devices panel.
 */
@media (max-width: 768px) {
    .app-container { height: auto; min-height: 100vh; }
    .app-body { overflow: visible; }
    .main-content { min-height: 0; }
    /* In the frame layout one of these scrolls and the other clips, each around something
       that scrolls inside it. In flow nothing does, so either value here would be a scrollbar
       or a clip with nothing behind it -- and this way the flow layout holds whatever a later
       change puts inside them, rather than resting on every descendant being content-sized. */
    .left-panel, .right-panel { overflow: visible; }
}
