:root {
    --bg: #0a0a1a;
    --panel: #12122a;
    --panel2: #1a1a3a;
    --border: #2a2a5a;
    --text: #e0e0f0;
    --text2: #a0a0c0;
    --accent: #4a9eff;
    --accent2: #00c8a0;
    --danger: #ff4a6a;
    --warning: #ffaa00;
    --success: #00d080;
    --steel: #8ab4c8;
    --stainless: #a0c8d0;
    --aluminum: #c0d0d8;
    --font: 'Segoe UI', system-ui, sans-serif;
    --mono: 'Cascadia Code', 'JetBrains Mono', 'Consolas', monospace;
}
* { margin:0; padding:0; box-sizing:border-box; }
/* Mobile Safari and Chrome "boost" text in wide viewports, inflating font
   sizes on the fly. In a dense engineering tool that silently breaks every
   size the layout was measured against - a 9px viz label becomes 12px and
   overflows its cell. Pinned to 100% so the CSS decides, not the browser. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
body {
    font-family: var(--font);
    background: var(--bg);
    color: var(--text);
    height: 100vh;
    /* `dvh` is the dynamic viewport: it tracks the mobile browser's collapsing
       address bar. `100vh` on a phone is the height of the screen with the
       bars HIDDEN, so the footer sat below the fold and the page could not be
       scrolled to it - the shell is `overflow: hidden`, so it was simply
       unreachable. The vh line above stays as the fallback for any engine
       without dvh. */
    height: 100dvh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    user-select: none;
    /* Respect notches and home indicators: `env()` resolves to 0 everywhere
       else, so this needs no media query. Applied to the two fixed rows of
       the shell rather than to body, or the padding would also inset the
       scrolling panes between them. */
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
}
/* No blue tap flash over a control that already shows a pressed state. */
a, button, input, select, textarea, label, [role="button"] { -webkit-tap-highlight-color: transparent; }
/* `manipulation` removes the ~300ms double-tap-zoom delay without disabling
   pinch-zoom on the page. The canvas opts out separately with `touch-action:
   none`, because it handles its own gestures. */
button, .btn, input, select, textarea, label, [role="button"] { touch-action: manipulation; }
::-webkit-scrollbar { width:6px; height:6px; }
::-webkit-scrollbar-track { background:var(--panel); }
::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
::-webkit-scrollbar-thumb:hover { background:#3a3a6a; }

/* Number inputs: drop the native up/down spinner arrows everywhere (table
   cells, combos grid and dialogs) — values are typed, arrows just steal space. */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}
input[type="number"] {
    -moz-appearance: textfield;
    -webkit-appearance: none;
    appearance: textfield;
}

.vsplit {
    width: 5px;
    flex-shrink: 0;
    cursor: col-resize;
    background: var(--border);
    transition: background 0.15s;
}
.vsplit:hover, .vsplit.active { background: var(--accent); }

/* The resize handles are now real controls: focusable, labelled, and operable
   with the arrow keys (see attachResizers in core.js). A 5 px bar is below the
   comfortable pointer target size and has no focus ring of its own, so keyboard
   users had no way to reach it and touch users had nothing to grab.
   - `touch-action: none` stops the browser claiming the drag as a scroll, which
     is what made a touch drag over a handle pan the page instead of resizing.
   - the ::before pseudo-element is the HIT AREA, not the visible line: it widens
     the grab target to 14 px while the drawn rule stays 5 px, so the layout and
     every measured offset are unchanged. */
.vsplit {
    position: relative;
    touch-action: none;
}
.vsplit::before {
    content: '';
    position: absolute;
    inset: 0 -5px;
    cursor: col-resize;
}
.vsplit:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}
/* A persistent hairline so a focused-but-not-hovered handle is still visible as
   a handle rather than disappearing back into the border colour. */
.vsplit:focus {
    background: var(--accent);
}
@media (max-width: 900px) {
    /* No handles exist in the stacked layout - the panes are full width and
       ordered vertically - so the focus ring would outline nothing. */
    .vsplit { display: none; }
}

.status-bar {
    background: var(--panel);
    border-top: 1px solid var(--border);
    padding: 4px 16px;
    display: flex;
    gap: 20px;
    font-size: 10px;
    color: var(--text2);
    flex-shrink: 0;
    letter-spacing: 0.3px;
}
.status-bar span { color: var(--accent); font-family: var(--mono); }

/* 🔴 THE ASSUMPTION REGISTER'S CONTROL, IN THE BOTTOM-RIGHT (owner,
   2026-10-16: "Move Assump button to the app's bottom right", then relabel
   "Assumptions" to "A", then "make button 'A' small to fit the bottom status
   bar"). Three asks, and the third is the reason the first needed the second:
   the strip is 22px tall and 10px type, and "Assump" does not fit in it.

   `.status-right` PUSHES RIGHT with `margin-left: auto` rather than by being
   the last flex item — the strip wraps on a narrow phone (see the 320px rule
   below), and on the second row "last item" would put the register under the
   LEFT edge. It is also un-styled against `.status-bar span`, which colours
   every span in the strip accent-blue and sets it in mono: the wrapper exists
   only to hold the button, so it takes neither.

   🔴 THE BADGE IS NOT OPTIONAL. A one-character label with no number is a
   control whose only explanation is its tooltip, and the whole reason this
   control exists is that the register must be visible without being opened. */
.status-bar .status-right {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    color: inherit;
    font-family: inherit;
}

.status-assump {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    padding: 1px 5px;
    border: 1px solid var(--border);
    border-radius: 3px;
    background: var(--panel2);
    color: var(--text);
    font-family: var(--mono);
    font-size: 10px;
    font-weight: 700;
    line-height: 1.2;
    letter-spacing: 0.3px;
    cursor: pointer;
}
.status-assump:hover { background: var(--panel3, var(--panel2)); border-color: var(--accent); }
.status-assump:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.status-assump .btn-count {
    min-width: 13px;
    padding: 0 3px;
    border-radius: 7px;
    background: var(--warning);
    color: #1c1600;
    font-size: 9px;
    font-weight: 700;
    line-height: 13px;
    text-align: center;
}

.btn {
    padding: 7px 14px;
    border: 1px solid var(--border);
    background: var(--panel2);
    color: var(--text);
    border-radius: 6px;
    cursor: pointer;
    font-size: 12px;
    font-weight: 500;
    transition: all 0.2s;
    white-space: nowrap;
    letter-spacing: 0.3px;
}
.btn:hover { border-color:var(--accent); background:#222244; color:#fff; }
/* Undo/Redo sit disabled most of the time. Without this they still take the
   hover treatment, so the toolbar offers a target that does nothing. */
.btn:disabled { opacity: 0.35; cursor: default; pointer-events: none; border-color: var(--border); background: var(--panel2); color: var(--text); }
.btn.primary { background:var(--accent); border-color:var(--accent); color:#000; font-weight:600; }
.btn.primary:hover { background:#6ab0ff; }
.btn.success { background:var(--success); border-color:var(--success); color:#000; font-weight:600; }
.btn.success:hover { background:#00e090; }
.btn.danger { background:transparent; border-color:var(--danger); color:var(--danger); }
.btn.danger:hover { background:var(--danger); color:#fff; }
.btn.small { padding:5px 10px; font-size:11px; }
.btn.active { border-color:var(--accent); background:#1a3a5a; color:var(--accent); }

#app-main {
    display: flex;
    flex: 1;
    overflow: hidden;
}        :root {
            --bg: #0a0a1a;
            --panel: #12122a;
            --panel2: #1a1a3a;
            --border: #2a2a5a;
            --text: #e0e0f0;
            --text2: #a0a0c0;
            --accent: #4a9eff;
            --accent2: #00c8a0;
            --danger: #ff4a6a;
            --warning: #ffaa00;
            --success: #00d080;
            --steel: #8ab4c8;
            --stainless: #a0c8d0;
            --aluminum: #c0d0d8;
            --font: 'Segoe UI', system-ui, -apple-system, sans-serif;
            --mono: 'Cascadia Code', 'JetBrains Mono', 'Consolas', monospace;
        }
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: var(--font);
            background: var(--bg);
            color: var(--text);
            height: 100vh;
            /* This block restates the whole shell and comes LAST in the file,
               so at equal specificity it wins over the `height: 100dvh` set on
               the real `body` rule near the top - the dvh fix there was being
               silently overridden on every load, not only under a theme. Caught
               by A50 resolving the cascade rather than grepping for `dvh`.
               Kept as a fallback for engines without dvh, same as above. */
            height: 100dvh;
            overflow: hidden;
            display: flex;
            flex-direction: column;
            user-select: none;
            -webkit-user-select: none;
        }
        ::-webkit-scrollbar {
            width: 6px;
            height: 6px;
        }
        ::-webkit-scrollbar-track {
            background: var(--panel);
        }
        ::-webkit-scrollbar-thumb {
            background: var(--border);
            border-radius: 3px;
        }
        ::-webkit-scrollbar-thumb:hover {
            background: #3a3a6a;
        }

        /* Header */
        .header {
            background: var(--panel);
            border-bottom: 1px solid var(--border);
            padding: 8px 16px;
            display: flex;
            align-items: center;
            gap: 16px;
            flex-shrink: 0;
            z-index: 10;
        }
        .header h1 {
            font-size: 18px;
            font-weight: 700;
            letter-spacing: 1px;
            background: linear-gradient(135deg, var(--accent), var(--accent2));
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            white-space: nowrap;
        }
        .header .subtitle {
            font-size: 11px;
            color: var(--text2);
            white-space: nowrap;
        }
        .header .btn-group {
            display: flex;
            gap: 6px;
            margin-left: auto;
            flex-wrap: wrap;
        }
        .btn {
            padding: 7px 14px;
            border: 1px solid var(--border);
            background: var(--panel2);
            color: var(--text);
            border-radius: 6px;
            cursor: pointer;
            font-size: 12px;
            font-weight: 500;
            transition: all 0.2s;
            white-space: nowrap;
            letter-spacing: 0.3px;
        }
        .btn:hover {
            border-color: var(--accent);
            background: #222244;
            color: #fff;
        }
        .btn:disabled {
            opacity: 0.35;
            cursor: default;
            pointer-events: none;
            border-color: var(--border);
            background: var(--panel2);
            color: var(--text);
        }
        .btn.primary {
            background: var(--accent);
            border-color: var(--accent);
            color: #000;
            font-weight: 600;
        }
        .btn.primary:hover {
            background: #6ab0ff;
        }
        .btn.success {
            background: var(--success);
            border-color: var(--success);
            color: #000;
            font-weight: 600;
        }
        .btn.success:hover {
            background: #00e090;
        }
        .btn.danger {
            background: transparent;
            border-color: var(--danger);
            color: var(--danger);
        }
        .btn.danger:hover {
            background: var(--danger);
            color: #fff;
        }
        .btn.small {
            padding: 5px 10px;
            font-size: 11px;
        }
        .btn.active {
            border-color: var(--accent);
            background: #1a3a5a;
            color: var(--accent);
        }

        /* Main Layout */
        .main {
            display: flex;
            flex: 1;
            overflow: hidden;
            gap: 0;
        }
        .panel {
            background: var(--panel);
            border: 1px solid var(--border);
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }
        .panel-left {
            width: 320px;
            min-width: 280px;
            max-width: 420px;
            border-right: none;
            overflow: auto;
        }
        .panel-center {
            flex: 1;
            position: relative;
            border-right: none;
        }
        .panel-right {
            width: 340px;
            min-width: 280px;
            max-width: 500px;
            border-left: 1px solid var(--border);
            overflow: auto;
        }

        .panel-header {
            padding: 10px 14px;
            background: var(--panel2);
            border-bottom: 1px solid var(--border);
            display: flex;
            align-items: center;
            gap: 8px;
            flex-shrink: 0;
            cursor: pointer;
        }
        .panel-header h3 {
            font-size: 13px;
            font-weight: 600;
            letter-spacing: 0.5px;
            text-transform: uppercase;
            color: var(--accent);
        }
        .panel-body {
            flex: 1;
            overflow-y: auto;
            padding: 10px;
            display: flex;
            flex-direction: column;
            gap: 8px;
        }

        /* Tabs */
        .tabs {
            display: flex;
            gap: 0;
            border-bottom: 1px solid var(--border);
            flex-shrink: 0;
            flex-wrap: wrap;
        }
        /* ---- FOCUS VISIBILITY -------------------------------------------
           Every keyboard-reachable control needs a visible focus indicator.
           Two gaps this closes:
             - there was NO focus rule for .btn / button anywhere, so the only
               indication was whatever the UA drew, which on the dark themes is
               a low-contrast ring;
             - the tab bars and the row delete controls had no tabindex at all,
               so they could not be focused and had no indicator to show.
           Uses --accent so it follows the theme, matching the idiom already
           used by .dview-b:focus-visible and .fview-b:focus-visible lower
           down. :focus-visible rather than :focus so a mouse click does not
           leave a ring behind, but a keyboard press always does.
           outline-offset keeps the ring clear of the element's own border so it
           stays visible on the dense table rows. */
        .btn:focus-visible,
        button:focus-visible,
        .tab:focus-visible,
        select:focus-visible,
        #viewport:focus-visible {
            outline: 2px solid var(--accent);
            outline-offset: 1px;
        }
        /* The delete control's own hover fill sits behind the glyph, so the
           ring is offset further to stay off it. */
        .data-table .remove-btn:focus-visible {
            outline: 2px solid var(--danger);
            outline-offset: 2px;
        }

        .tab {
            padding: 8px 14px;
            cursor: pointer;
            font-size: 11px;
            font-weight: 500;
            color: var(--text2);
            border-bottom: 2px solid transparent;
            transition: all 0.2s;
            white-space: nowrap;
            letter-spacing: 0.3px;
        }
        .tab:hover {
            color: var(--text);
            background: rgba(74, 158, 255, 0.05);
        }
        .tab.active {
            color: var(--accent);
            border-bottom-color: var(--accent);
            background: rgba(74, 158, 255, 0.08);
        }
        .tab-content {
            display: none;
            padding: 10px 0;
            flex: 1;
            overflow-y: auto;
        }
        .tab-content.active {
            display: block;
        }

        /* Tables */
        .data-table {
            width: 100%;
            border-collapse: separate;
            border-spacing: 0;
            font-size: 11px;
            margin-bottom: 8px;
        }
        .data-table th {
            background: var(--panel2);
            color: var(--accent);
            padding: 6px 6px;
            text-align: center;
            font-weight: 600;
            font-size: 10px;
            letter-spacing: 0.3px;
            text-transform: uppercase;
            border-bottom: 1px solid var(--border);
            position: sticky;
            top: 0;
            z-index: 30;
            box-shadow: inset 0 1px 0 rgba(0,0,0,0.4);
        }
        .data-table thead th { position: sticky; top: 0; z-index: 31; }
        .data-table td {
            padding: 5px 6px;
            border-bottom: 1px solid rgba(42, 42, 90, 0.4);
            color: var(--text);
        }
        .data-table tr:hover td {
            background: rgba(74, 158, 255, 0.04);
        }
        .data-table input,
        .data-table select {
            background: var(--panel2);
            border: 1px solid var(--border);
            color: var(--text);
            padding: 3px 5px;
            border-radius: 3px;
            font-size: 11px;
            width: 100%;
            font-family: var(--mono);
        }
        .data-table input:focus,
        .data-table select:focus {
            outline: none;
            border-color: var(--accent);
        }
        .data-table .num {
            text-align: center;
            font-family: var(--mono);
        }
        .data-table .num input {
            text-align: center;
        }
        /* The delete control in a data row.
           It was a <td onclick>, which put a DESTRUCTIVE action behind a
           non-focusable element with no accessible name and no role: it was
           unreachable by keyboard and announced as a table cell whose content
           happened to be a cross. It is now a real <button>, so it is focusable,
           activates on Enter and Space, and carries an aria-label naming what
           it deletes ("Delete node 3"), not just a glyph.
           Because it is a button rather than a cell, the UA button box has to be
           reset or the row gains a border, padding and a background it never
           had; the painted result is deliberately identical to before. */
        .data-table td.remove-cell {
            text-align: center;
            width: 24px;
            padding: 0;
        }
        .data-table .remove-btn {
            background: none;
            border: 0;
            margin: 0;
            padding: 0;
            font: inherit;
            font-size: 14px;
            line-height: 1;
            color: var(--danger);
            cursor: pointer;
            width: 24px;
            display: inline-block;
        }
        .data-table .remove-btn:hover {
            background: rgba(255, 74, 106, 0.2);
            border-radius: 3px;
        }

        /* Canvas */
        #viewport {
            width: 100%;
            height: 100%;
            cursor: grab;
            background: radial-gradient(ellipse at center, #0d0d25 0%, #060612 100%);
        }
        #viewport:active {
            cursor: grabbing;
        }
        .canvas-overlay {
            position: absolute;
            top: 10px;
            left: 10px;
            display: flex;
            flex-direction: column;
            gap: 4px;
            pointer-events: none;
            z-index: 5;
        }
        .canvas-overlay .info-tag {
            background: rgba(18, 18, 42, 0.85);
            border: 1px solid var(--border);
            padding: 4px 10px;
            border-radius: 4px;
            font-size: 10px;
            color: var(--text2);
            letter-spacing: 0.3px;
            pointer-events: auto;
        }
        .canvas-overlay .info-tag span {
            color: var(--accent);
            font-family: var(--mono);
        }
        .legend {
            position: absolute;
            bottom: 10px;
            left: 10px;
            display: flex;
            gap: 12px;
            pointer-events: none;
            z-index: 5;
            flex-wrap: wrap;
        }
        .legend-item {
            display: flex;
            align-items: center;
            gap: 5px;
            font-size: 10px;
            color: var(--text2);
            background: rgba(18, 18, 42, 0.8);
            padding: 3px 8px;
            border-radius: 4px;
            border: 1px solid var(--border);
        }
        .legend-dot {
            width: 8px;
            height: 8px;
            border-radius: 50%;
            display: inline-block;
        }

        /* Results */
        .result-summary {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 6px;
            margin-bottom: 10px;
        }
        .result-card {
            background: var(--panel2);
            border: 1px solid var(--border);
            border-radius: 6px;
            padding: 8px 10px;
            text-align: center;
        }
        .result-card .label {
            font-size: 10px;
            color: var(--text2);
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }
        .result-card .value {
            font-size: 20px;
            font-weight: 700;
            font-family: var(--mono);
            margin-top: 2px;
        }
        .result-card .value.pass {
            color: var(--success);
        }
        .result-card .value.fail {
            color: var(--danger);
        }
        .result-card .value.warn {
            color: var(--warning);
        }

        .util-bar {
            height: 6px;
            background: var(--panel2);
            border-radius: 3px;
            overflow: hidden;
            margin: 2px 0;
        }
        .util-bar-fill {
            height: 100%;
            border-radius: 3px;
            transition: width 0.4s;
        }
        .util-bar-fill.ok {
            background: var(--success);
        }
        .util-bar-fill.warn {
            background: var(--warning);
        }
        .util-bar-fill.fail {
            background: var(--danger);
        }

        .code-check-table td {
            font-size: 10px;
            padding: 4px 4px;
        }
        .code-check-table .pass-text {
            color: var(--success);
            font-weight: 600;
        }
        .code-check-table .fail-text {
            color: var(--danger);
            font-weight: 600;
        }

        .vsplit {
            width: 5px;
            flex-shrink: 0;
            cursor: col-resize;
            background: var(--border);
            transition: background 0.15s;
        }
        .vsplit:hover,
        .vsplit.active { background: var(--accent); }

        /* Status bar */
        .status-bar {
            background: var(--panel);
            border-top: 1px solid var(--border);
            padding: 4px 16px;
            display: flex;
            gap: 20px;
            font-size: 10px;
            color: var(--text2);
            flex-shrink: 0;
            letter-spacing: 0.3px;
        }
        .status-bar span {
            color: var(--accent);
            font-family: var(--mono);
        }

        /* Modal
           ------------------------------------------------------------------
           The .modal-overlay / .modal / .form-group / .btn-row rules that used
           to be restated here have MOVED to styles/components/modal.css, which
           is the one place a modal's appearance is defined.

           They were duplicated verbatim, which is why register item 26 (the
           checkbox-in-a-form-group cascade fix) had to be applied TWICE - once
           in each copy. A rule that must be edited in two places to be correct
           will eventually be edited in one, and the surviving copy silently wins
           because index.html loads modal.css AFTER this file. The duplication
           bought nothing: the two blocks were equivalent, so removing this one
           changes nothing a user can see.

           tests/modal-css.mjs (A28) now asserts the rule is defined in exactly
           ONE sheet, so the duplication cannot come back. */
        /* .report-text was defined here AND in styles/components/modal.css.
           index.html loads main.css FIRST and modal.css LAST, so modal.css
           always won and this copy was dead - a rule that looks authoritative
           and changes nothing, which is worse than no rule at all. modal.css
           now owns it outright (the STAAD output viewer needs `white-space:
           pre` and a flexible height, neither of which fitted here). Do not
           re-add a copy: edit styles/components/modal.css. */
        .empty-state {
            text-align: center;
            padding: 20px;
            color: var(--text2);
            font-size: 12px;
        }
        input[type="color"] {
            padding: 2px;
            border: 1px solid var(--border);
            border-radius: 3px;
            background: var(--panel2);
            cursor: pointer;
            width: 30px;
            height: 25px;
        }
        .data-table td, .data-table th { text-align: center; }
        .data-table .num, .data-table .num input { text-align: center; }
        .data-table input[type="number"] { text-align: center; }
        .viz-bar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
        .viz-chip { display: inline-flex; align-items: center; gap: 4px; font-size: 10px;
            color: var(--text2); background: rgba(18,18,42,0.85); border: 1px solid var(--border);
            padding: 2px 7px; border-radius: 4px; cursor: pointer; pointer-events: auto; user-select: none; }
        .viz-chip input { accent-color: var(--accent); margin: 0; }
        .viz-chip input[type="range"] { width: 70px; }
        .viz-chip span { min-width: 30px; text-align: right; font-family: var(--mono); }
        .viz-chip.on { color: #fff; border-color: var(--accent); }
        .viz-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 5px;
            margin-top: 6px; pointer-events: auto; max-width: 940px; width: 100%; }
        .viz-grid .vc { background: rgba(18,18,42,0.85); border: 1px solid var(--border);
            border-radius: 5px; padding: 3px 5px; }
        .viz-grid .chk { display: flex; align-items: center; gap: 3px; font-size: 9px;
            color: var(--text2); cursor: pointer; margin-bottom: 3px; white-space: nowrap; }
        .viz-grid .chk input { accent-color: var(--accent); margin: 0; }
        .viz-grid .chk.on { color: #fff; }
        .viz-grid .sz { display: flex; align-items: center; gap: 3px; }
        .viz-grid .sz input[type="range"] { flex: 1 1 auto; min-width: 24px; margin: 0; }
        .viz-grid .sz b { font-family: var(--mono); font-size: 9px; min-width: 26px; text-align: right; color: var(--accent2); }
        .panel-left { width: 33.333%; min-width: 0; max-width: none; flex: 1 1 0; }
        .panel-center { flex: 1 1 0; }
        .panel-right { width: 33.333%; min-width: 0; max-width: none; flex: 1 1 0; }

/* ============================================================
   Custom-element host layout (component refactor)
   The original layout flexed .panel / #viewport children directly.
   Here the <app-*> hosts are the flex children and their inner
   light-DOM content fills them.
   ============================================================ */
#app-main {
    display: flex;
    align-items: stretch;
    min-height: 0;
}
app-header { display: block; }
app-header .header { height: 100%; }
app-panel {
    display: flex;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
    overflow: hidden;
    background: var(--panel);
}
app-panel[position="left"],
app-panel#panel-left  { flex: 1 1 0; }
app-panel[position="right"],
app-panel#panel-right { flex: 1 1 0; }
app-panel .panel-header { flex-shrink: 0; }
app-panel .tabs { flex-shrink: 0; }
app-panel .panel-body {
    flex: 1 1 auto;
    min-height: 0;
}
app-canvas {
    display: block;
    position: relative;
    flex: 1 1 0;
    min-width: 0;
    min-height: 0;
}
app-modal { display: block; }

/* Row currently selected / highlighted from the canvas or by clicking it */
.data-table tbody tr.row-picked {
    background: rgba(255, 210, 74, 0.14) !important;
    outline: 1px solid rgba(255, 210, 74, 0.7);
}
.data-table tbody tr { cursor: default; }
/* Results rows identify members/nodes on the 3D canvas: hover glows, click pins
   the breathing halo (the picked row shows the amber outline above). */
.data-table tbody tr.res-row { cursor: pointer; }

/* Load Combinations editor: 'Use' tick column + rows the user unticked (excluded
   from the structural analysis and code check, but kept so they can be re-ticked). */
.data-table th.c-use,
.data-table td.c-use { width: 34px; text-align: center; }
.data-table tr.combo-off td { opacity: 0.45; }
.data-table tr.combo-off td input { opacity: 0.6; }

/* Loads table: flat one-row-per-case spreadsheet (Case | Action | Description |
   Force | Target | Dir | Value | Units | ✕) */
#loads-table { white-space: nowrap; }
#loads-table th,
#loads-table td { text-align: center; vertical-align: middle; white-space: nowrap; }
#loads-table th { font-weight: 600; }
#loads-table select,
#loads-table input[type="text"],
#loads-table input[type="number"] {
    box-sizing: border-box;
    height: 24px;
    padding: 1px 6px;
    font-size: 11px;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--panel);
    color: var(--text);
    width: 100%;
}
/* 🔴 CENTRED, NOT LEFT (owner, 2026-10-13: "justify Loads tabulation to all
   centre justify"). This and the `.data-table` rule further down were both
   `text-align: left`, and they existed for a real reason — see the long note at
   the `.data-table input[type="text"]` block about px widths — but that reason
   was WIDTH, not alignment. Centring the text of a fixed-layout cell does not
   change its width, does not make it overflow, and does not make it touch the
   next column's control. The Loads table is a spreadsheet, and every other cell
   in it was already centred; two free-text columns reading left while the rest
   centre read as unfinished rather than deliberate.

   A50 asserted `text-align: left` here. That assertion was INVERTED rather than
   deleted, so the reversal is a decision on the record and not a missing test. */
#loads-table input[type="text"] { text-align: center; }
#loads-table select option { text-align: center; }
#loads-table input[type="number"] { text-align: center; }

/* Per-column alignment + fixed widths (Description keeps a guaranteed width so
   descriptions/placeholders stay readable; it absorbs extra space when the
   panel is wide). */
#loads-table td.c-no, #loads-table th.c-no { text-align: center; }
#loads-table td.c-no { font-family: var(--mono); font-weight: 600; color: var(--accent2); }
#loads-table th.c-action, #loads-table td.c-action { text-align: center; }
#loads-table th.c-force, #loads-table td.c-force { text-align: center; }
#loads-table th.c-target, #loads-table td.c-target { text-align: center; }
#loads-table th.c-dir, #loads-table td.c-dir { text-align: center; }
#loads-table th.c-val, #loads-table td.c-val { text-align: center; }
#loads-table th.c-unit, #loads-table td.c-unit { text-align: center; }
#loads-table td.c-unit { color: var(--text2); font-size: 10px; }
#loads-table th.c-rm, #loads-table td.c-rm { text-align: center; }

#loads-table { table-layout: fixed; width: 100%; }
#loads-table th.c-no { width: 36px; }
#loads-table th.c-action { width: 62px; }
#loads-table th.c-desc { width: 132px; }
#loads-table th.c-force { width: 76px; }
#loads-table th.c-target { width: 108px; }
#loads-table th.c-dir { width: 58px; }
#loads-table th.c-val { width: 84px; }
#loads-table th.c-unit { width: 48px; }
#loads-table th.c-rm { width: 26px; }

/* SELFWEIGHT checkbox control (Loads tab): explicit opt-in for self weight */
.sw-toggle {
    display: flex;
    gap: 8px;
    align-items: center;
    margin: 8px 10px 0;
    padding: 7px 9px;
    font-size: 11px;
    line-height: 1.4;
    color: var(--text);
    background: var(--panel2);
    border: 1px solid var(--border);
    border-radius: 6px;
    cursor: pointer;
    user-select: none;
}
.sw-toggle input[type="checkbox"] {
    margin: 0;
    accent-color: var(--accent2);
    cursor: pointer;
}
.sw-toggle:has(input:checked) {
    border-color: rgba(0, 208, 128, 0.5);
    box-shadow: inset 3px 0 0 rgba(0, 208, 128, 0.55);
}
.sw-toggle:has(input:disabled) { opacity: 0.8; cursor: not-allowed; }
.sw-toggle input:disabled { cursor: not-allowed; }
.sw-toggle b { color: var(--accent); }

/* Locked auto 'G · SELF WEIGHT' row (self-weight component inside the permanent case G; no edit/delete controls) */
#loads-table tr.sw-row td { background: rgba(0, 208, 128, 0.05); }
#loads-table tr.sw-row:hover td { background: rgba(0, 208, 128, 0.09); }
#loads-table .case-chip {
    display: block;
    height: 24px;
    line-height: 22px;
    padding: 0 6px;
    font-size: 11px;
    font-family: var(--mono);
    color: #00d080;
    background: var(--panel);
    border: 1px solid rgba(0, 208, 128, 0.45);
    border-radius: 4px;
    text-align: center;
}
#loads-table .sw-locked { font-size: 11px; color: var(--text); }
/* One load case covering members with different line loads: the magnitude cell
   is a range, not a number, so it is styled like the other derived readouts and
   the split control sits next to it. See forceCell/valueCell in core.js. */
#loads-table .val-range {
    font-family: var(--mono);
    color: var(--accent2);
    font-weight: 600;
    font-size: 11px;
    white-space: nowrap;
}
#loads-table .split-btn {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text2);
    border-radius: 3px;
    cursor: pointer;
    font-size: 11px;
    line-height: 1;
    padding: 2px 4px;
    margin-left: 4px;
}
#loads-table .split-btn:hover { color: var(--accent); border-color: var(--accent); }
#loads-table input.sw-locked.sw-desc {
    background: transparent;
    border-color: transparent;
    color: var(--text);
    -webkit-text-fill-color: var(--text);
    cursor: default;
}
#loads-table .sw-locked.sw-auto {
    font-family: var(--mono);
    color: var(--accent2);
    font-weight: 600;
}
#loads-table .sw-locked.sw-chip {
    display: inline-block;
    min-width: 26px;
    text-align: center;
    font-family: var(--mono);
    font-weight: 600;
    font-size: 11px;
    color: #00d080;
    background: var(--panel);
    border: 1px solid rgba(0, 208, 128, 0.45);
    border-radius: 4px;
    padding: 0 6px;
    height: 22px;
    line-height: 20px;
}
/* ⚖ Combos editor — factor grid over the load cases */
table.combo-grid { white-space: nowrap; }
table.combo-grid th { font-weight: 600; font-size: 10px; vertical-align: bottom; }
table.combo-grid td.c-no,
table.combo-grid th.c-no { text-align: center; font-family: var(--mono); }
table.combo-grid td.c-no { color: var(--accent2); }
table.combo-grid input.c-fac {
    /* width comes from --cell-w in the uniform-sizing pass below. */
    text-align: right;
    font-family: var(--mono);
    font-size: 11px;
}
/* .c-cname width moved to the uniform-sizing pass at the end of this file,
   where the free-text name column is the one documented exception to
   --cell-w. It is sized to the longest name in JS (sizeComboNameColumn). */

/* Load rows bind to user-defined load cases: kind chip + case picker in one cell.
   Descriptions are edited in the 🗂 Load Cases list (per case, never mirrored). */
#loads-table td.c-case-cell { white-space: nowrap; }
#loads-table td.c-case-cell .case-chip {
    display: inline-block;
    width: 30px;
    margin-right: 4px;
    vertical-align: middle;
}
#loads-table td.c-case-cell select {
    max-width: 200px;
    vertical-align: middle;
}
#loads-table .sw-row td.c-case-cell .sw-desc {
    display: inline-block;
    vertical-align: middle;
    font-weight: 600;
    color: var(--text);
}

#loads-table td.case-empty {
    font-size: 10px;
    font-style: italic;
    color: var(--text2);
    opacity: 0.75;
    padding: 2px 10px 6px 14px;
    white-space: normal;
}

/* Results: governing-combination sub-row shown under each value row */
.data-table tr.gov-row td {
    padding: 1px 6px 5px 6px;
    font-size: 9px;
    line-height: 1.3;
    color: var(--text2);
    border-bottom: 1px solid var(--border);
    white-space: normal;
    word-break: break-word;
}
.data-table tr.gov-row td.gov-label {
    color: var(--accent);
    font-weight: 700;
    text-transform: uppercase;
    font-size: 8px;
    white-space: nowrap;
    vertical-align: middle;
}
.data-table tr.gov-row td.gov-cell {
    text-align: center;
    font-family: var(--mono);
}
.data-table tr.gov-row:hover td { background: transparent; }

/* Sticky table headers app-wide: let the tab content be the real scroller so the
   header stays pinned; neutralise small horizontal-only wrappers that otherwise
   trap `position: sticky` in a non-scrolling box. */
.tab-content { overflow: auto; }
.tab-content > div[style*="overflow-x:auto"] { overflow: visible !important; }

/* Displacement sub-toggle: the single "Displ" tab hosting the Abs / Rel tables.
   Mirrors the .fview pill used by the Forces tab below so the two inline view
   switches read identically in the results pane. */
.dview {
    display: inline-flex;
    gap: 2px;
    padding: 2px;
    margin-bottom: 8px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--panel2);
}
.dview-b {
    padding: 4px 12px;
    border: 0;
    border-radius: 4px;
    background: transparent;
    color: var(--text2);
    font-family: inherit;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: .3px;
    cursor: pointer;
    white-space: nowrap;
}
.dview-b:hover { color: var(--text); background: rgba(74, 158, 255, .07); }
.dview-b.active { color: var(--accent); background: rgba(74, 158, 255, .14); }
.dview-b:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }
.data-table thead th { position: sticky; top: 0; z-index: 31; }

/* Extreme cell (forces / reactions / displacements). One shared marker with
   three different selection rules, per table:
     - Reactions, Forces Envelope, Forces Stations: BOTH the most positive and
       the most negative value of each column (via columnExtremes /
       isColumnExtreme), so up to two marked cells per column.
     - Displacements: the largest magnitude within each ROW, which is a
       per-row pick because a displacement row is one node's components. */
.data-table td.cell-max {
    background: rgba(255, 210, 74, 0.22);
    font-weight: 700;
    box-shadow: inset 0 0 0 1px rgba(255, 210, 74, 0.55);
}

/* Abs Displ "Max Displacement" table: a TWO-TIER header — the group row (Horizontal /
   Vertical / Resultant / Rotational) over the sub row (X / Y / Z, rX / rY / rZ). Both
   tiers are position:sticky, so the sub row is offset by the group row's height instead
   of pinning underneath it, keeping each sub heading under its own group while the
   table scrolls. 25px = 10px th font at the default line-height + 6px padding each side. */
.data-table thead tr.hd-grp th { top: 0; z-index: 32; }
.data-table thead tr.hd-sub th { top: 25px; z-index: 31; box-shadow: none; }

/* Unit line under a sub heading (mm / rad) — same trick as the Forces tab's .fx-hd. */
.data-table th .hd-u {
    display: block;
    font-size: 8px;
    font-weight: 400;
    line-height: 1.1;
    letter-spacing: 0.02em;
    color: var(--text2);
}

/* Forces tab view switch (Stations / Envelope). A compact two-button pill sized
   for the results pane — the app-wide .tab is tuned for the main tab bar and
   carries a border-bottom that would read as a second tab bar down here. */
.fview {
    display: inline-flex;
    gap: 2px;
    padding: 2px;
    margin-bottom: 8px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--panel2);
}
.fview-b {
    padding: 4px 12px;
    border: 0;
    border-radius: 4px;
    background: transparent;
    color: var(--text2);
    font-family: inherit;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: .3px;
    cursor: pointer;
    white-space: nowrap;
}
.fview-b:hover { color: var(--text); background: rgba(74, 158, 255, .07); }
.fview-b.active { color: var(--accent); background: rgba(74, 158, 255, .14); }

/* Pinned view toggle — Abs/Rel on the Displ tab, Stations/Envelope on Forces.
   Both tabulations run to thousands of rows, so the control that chooses the
   view has to stay reachable while you scroll: it pins to the top of the tab
   content, exactly like the table header does. Two details make it read as one
   header stack rather than two floating pieces:
     - the BAR sticks, not the pill. A sticky pill is still an inline-level box
       (sticky positioning needs a block-level one) and, even forced to block,
       a fit-content pill would let rows scroll past beside it.
     - the bar is filled with --panel2, the same colour the sticky th uses, so
       the toggle band and the column headings underneath join up.
   The toggle and the table header are independent sticky pins, so the header
   rows are offset DOWN by the bar's height (--sticky-head, measured at runtime
   because the bar is font-sized) instead of pinning underneath it. The default
   is 0 so a pane that has not been measured yet still lays out correctly. */
.dview-bar {
    position: sticky;
    top: 0;
    z-index: 34;                  /* over thead th (31) and hd-grp (32) */
    background: var(--panel2);
    margin: 0 0 6px;
    padding-bottom: 6px;
}
.dview-bar > .fview, .dview-bar > .dview { margin-bottom: 0; }
#rtab-displ, #rtab-forces { --sticky-head: 0px; }
#force-results .data-table thead th,
#dview-rel .data-table thead th { top: var(--sticky-head); }
/* Abs Displ is a TWO-TIER header, so its sub row keeps its 25px drop and
   simply hangs off the measured bar height instead of a fixed 0. */
#dview-abs .data-table thead tr.hd-grp th { top: var(--sticky-head); }
#dview-abs .data-table thead tr.hd-sub th { top: calc(var(--sticky-head) + 25px); }
.fview-b:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }

/* Result tabs: a result value, with the combination that governs it set beside
   or beneath it. Retained for the cell shape even where a tab now prints one
   value per cell (Reactions, Forces stations). */
.data-table th .fx-hd {
    display: block;
    font-size: 8px;
    font-weight: 400;
    letter-spacing: .02em;
    color: var(--text2);
}
.data-table td.fx-cell {
    text-align: center;
    white-space: nowrap;
    padding: 3px 7px;
    line-height: 1.35;
}
.data-table td.fx-cell .fx-v {
    display: block;
    font-family: var(--mono);
}
.data-table td.fx-cell .fx-lo { color: var(--text2); }
/* Unrestrained dof on the Reactions tab: no reaction exists (as distinct from zero). */
.data-table td.fx-cell .fx-free {
    color: var(--text2);
    opacity: .55;
    font-style: italic;
    font-family: var(--font, inherit);
}
.data-table td.fx-cell .fx-c {
    display: inline-block;
    margin-left: 4px;
    font-style: normal;
    font-size: 8px;
    color: var(--text2);
    opacity: .85;
}
/* Reactions tab: the cell holds ONE value (the row carries its sign), so the
   governing combination moves onto its own line beneath the number, STAAD-style.
   Wrapped and width-capped: a generated combo name like "ULS: 1.2G + 1.5Q + 0.6Q"
   would otherwise stretch the whole table sideways. */
.data-table td.fx-cell .rx-case {
    display: block;
    margin-left: 0;
    margin-top: 1px;
    font-size: 8px;
    white-space: normal;
    max-width: 150px;
}
/* Limit-state tag on each governing case (ULS vs SLS in one envelope). */
b.fx-ls {
    display: inline-block;
    margin-right: 3px;
    padding: 0 3px;
    border-radius: 2px;
    font-size: 7px;
    font-weight: 700;
    letter-spacing: .03em;
    vertical-align: 1px;
    color: var(--accent);
    background: color-mix(in srgb, var(--accent) 16%, transparent);
}
b.fx-ls.sls {
    color: var(--accent2);
    background: color-mix(in srgb, var(--accent2) 16%, transparent);
}
i.fx-int {
    font-style: normal;
    font-size: 8px;
    vertical-align: super;
    color: var(--warning);
    margin-left: 1px;
}

/* Project metadata (Details tab): match the rest of the app's controls */
#tab-details .form-group {
    display: grid;
    gap: 3px;
}
#tab-details .form-group label {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.4px;
    text-transform: uppercase;
    color: var(--text2);
}
#tab-details .form-group input {
    box-sizing: border-box;
    width: 100%;
    height: 28px;
    padding: 4px 8px;
    font-size: 12px;
    font-family: var(--font);
    color: var(--text);
    background: var(--panel2);
    border: 1px solid var(--border);
    border-radius: 4px;
}
#tab-details .form-group input:focus {
    outline: none;
    border-color: var(--accent);
}
#tab-details input[type="date"] { color-scheme: dark; }
#tab-details ::placeholder { color: var(--text2); opacity: 0.7; }

/* ---- Theme picker & eye-friendly themes ---- */
.theme-picker {
    height: 28px;
    font-size: 12px;
    color: var(--text);
    background: var(--panel2);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 2px 6px;
}
.theme-picker option { background: var(--panel2); color: var(--text); }

/* Low-glare light theme (soft paper, comfortable for long hours) */
html[data-theme="light"] {
    --bg: #f4f2ec;
    --panel: #ebe7de;
    --panel2: #fdfcf9;
    --border: #cfc8b8;
    --text: #2a2825;
    --text2: #5f5a50;
    --accent: #1167a8;
    --accent2: #0e8a6c;
    --danger: #b23a48;
    --warning: #a06f0a;
    --success: #0e8a50;
    --steel: #37616f;
    --stainless: #567c82;
    --aluminum: #7f8890;
}
html[data-theme="light"] input[type="date"] { color-scheme: light; }

/* Sepia theme (warm, reduced blue light, gentle contrast) */
html[data-theme="sepia"] {
    --bg: #f6ecd8;
    --panel: #efe1c6;
    --panel2: #faf3e4;
    --border: #d8c5a3;
    --text: #3a3327;
    --text2: #77684f;
    --accent: #8a5a10;
    --accent2: #5f7f2f;
    --danger: #a0322f;
    --warning: #a06f0a;
    --success: #5f7f2f;
    --steel: #6c7a5a;
    --stainless: #7f8a6f;
    --aluminum: #9a9686;
}
html[data-theme="sepia"] input[type="date"] { color-scheme: light; }

/* HUD overlays (info tags, legend, viz/slider chips) follow the active theme */
.canvas-overlay .info-tag,
.legend-item,
.viz-grid .vc {
    background: var(--panel2);
    border-color: var(--border);
}
.canvas-overlay .info-tag {
    color: var(--text2);
}
.canvas-overlay .info-tag span {
    color: var(--accent);
}
.legend-item {
    color: var(--text2);
    background: var(--panel2);
    border-color: var(--border);
}
.viz-grid .chk,
.viz-chip {
    color: var(--text);
}
.viz-chip.on,
.viz-grid .chk.on {
    color: var(--accent);
    border-color: var(--accent);
}
.viz-grid .sz b {
    color: var(--text2);
}




/* Wind case (W): highlight the Dir cell so its direction is set per load row */
#loads-table td.wind-dir select {
    border-color: #ffd257;
    box-shadow: 0 0 0 1px rgba(255, 210, 74, 0.7);
    background: rgba(255, 210, 74, 0.10);
}


/* ============================================================
   Responsive audit [#15]: the app is a three-pane desktop
   layout (left panel | canvas | right panel). Under ~900px the
   panes stack vertically so tables/panels stay usable and the
   canvas keeps a usable height; individual tables already scroll
   horizontally via their overflow-x:auto wrappers.
   ============================================================ */
@media (max-width: 900px) {
    #app-main {
        flex-direction: column;
        overflow-y: auto;
        /* Momentum scrolling on iOS. Without it a touch drag inside #app-main
           is delivered as a scroll with no inertia, which reads as the app
           lagging rather than as a page. */
        -webkit-overflow-scrolling: touch;
    }
    #app-main .vsplit { display: none; }
    #app-main app-panel {
        flex: 0 0 auto;
    }
    #app-main app-panel .panel-body {
        max-height: 42vh;
        /* dvh, not vh: on a phone the browser chrome is part of `vh` but not
           of what the user can see, so a `vh`-sized panel measured against the
           hidden-bars height runs under the address bar. Every `vh` in this
           block is a fixed row of a 100vh shell, so they compound. */
        max-height: 42dvh;
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
    }
    #app-main app-canvas {
        flex: 0 0 52vh;
        flex: 0 0 52dvh;
        min-height: 300px;
    }
}

/* ---- Phone: tables are the reason this app is hard on a small screen ------
   The stacked layout above gives every panel its own vertical scroll, but a
   data-table with 8+ columns is still wider than any phone. The wrappers
   already scroll horizontally; what was missing is momentum and a visible
   hint that there IS more to the right - a plain cut-off column just reads as
   a broken table. */
@media (max-width: 640px) {
    .data-table,
    table.combo-grid {
        -webkit-overflow-scrolling: touch;
    }
    /* THE FADE WAS HERE, ON `.tab-content > div[style*="overflow-x:auto"]`, and
       it DESTROYED the right-hand side of every input table instead of hinting
       at it.

       A mask paints transparency over the element's own border box and clips
       whatever falls outside it. That wrapper is 384px wide on a phone and the
       table inside it is 632px, so everything past ~92% of 384px was painted
       fully transparent - permanently, at every scroll position, because the
       wrapper does not scroll. `main.css` line 1020 forces it to
       `overflow: visible !important` so that `.tab-content` can own the sticky
       `thead`; that is why the real scroller is `.tab-content` and the wrapper
       is a stationary box with a hole in its right-hand side.

       The result, MEASURED at 390px on the Loads tab: scrolled fully right, the
       table's content ended at x=117 real px and everything past it was empty,
       so the last four columns never appeared no matter how far the user
       dragged. The SELFWEIGHT total and the Combos button vanished with them.
       See viz/table-probe/BROKEN-390-right.png against FIXED-390-right.png,
       captured by tools/table-scroll-probe.mjs at 390px.

       No fade is drawn here instead. The horizontal scrollbar is the honest
       affordance, and it is already reachable: it sits directly under the table
       it belongs to. Re-adding a fade needs an element that actually scrolls -
       which per line 1020 means `.tab-content`, and that box also contains the
       tab strip and the button rows, which DO fit on a phone and would be
       faded for no reason. */
    .data-table-scroll {
        -webkit-overflow-scrolling: touch;
    }
    /* The status bar's four items do not fit a 320px phone on one line and
       were forcing the page wider. They wrap instead. */
    .status-bar {
        flex-wrap: wrap;
        gap: 2px 12px;
        padding-left: calc(10px + env(safe-area-inset-left));
        padding-right: calc(10px + env(safe-area-inset-right));
        /* Bottom inset for the home indicator / gesture bar in landscape. */
        padding-bottom: calc(4px + env(safe-area-inset-bottom));
    }
}

/* ---- Touch targets, general -----------------------------------------------
   The tail of the sub-44px population after the component-specific passes.
   MEASURED at 390px and 844px: `.btn.small` (the header's Clear) at 50x24, the
   panel tab strip at 62x33, and bare data-table inputs at 89x21.

   34px throughout, not 44. These all sit inside rows and tab strips that would
   grow out of proportion: a 44px table input makes every row 44px tall and
   halves how many rows a phone shows. 34px clears WCAG 2.5.8 AA (24px) with
   margin, which is the right trade for a dense professional tool - the header
   toolbar, which has room, does get the full 44. */
@media (pointer: coarse) {
    .btn.small { min-height: 34px; padding: 8px 12px; }
    .tab { min-height: 34px; display: inline-flex; align-items: center; }
    .data-table input,
    .data-table select,
    table.combo-grid input,
    table.combo-grid select {
        min-height: 34px;
    }
    /* Range sliders on the panel, not the canvas overlay. */
    .panel input[type="range"] { height: 34px; }
}

/* ============================================================
   Uniform table cells (final pass): every header and cell — and
   any control inside a cell — centres its text horizontally with
   cells vertically aligned. Placed last so it supersedes every
   earlier per-column override (loads table, combos grid, etc.).
   ============================================================ */
.data-table th,
.data-table td {
    text-align: center;
    vertical-align: middle;
}
.data-table input,
.data-table select {
    text-align: center;
}
.data-table .num input { text-align: center; }
.data-table tr.gov-row td,
.data-table td.gov-label,
.data-table td.gov-cell {
    text-align: center;
    vertical-align: middle;
}
table.combo-grid th,
table.combo-grid td {
    text-align: center;
    vertical-align: middle;
}
table.combo-grid input.c-fac,
table.combo-grid input.c-cname {
    text-align: center;
}


/* ============================================================
   Uniform control sizing [#table-consistency]
   ------------------------------------------------------------
   The input tables had FOUR different box widths, none of them
   shared:

     Nodes X/Y/Z      width:100%   <- inherited from .data-table
     Materials fy/fu  width:64px   (inline, core.js updateMaterialsTable)
     Members Roll     width:52px   (inline, core.js updateMembersTable)
     Combos factor    width:58px   (main.css input.c-fac)

   The Nodes case was the visible one: with no inline width it fell
   through to `.data-table input { width:100% }`, so in a 5-column
   table each X/Y/Z box stretched to fill a third of the panel while
   the Material table right beside it showed 64px boxes. Same control,
   same font, same padding, two sizes side by side.

   One token now owns the numeric-cell width. Selects deliberately
   keep width:100% - they carry text ("UB 200x100x22.7", "Grade 350",
   "Plate 10mm") that a 64px box would truncate, and the column they
   sit in has nothing else to give. The token is `--cell-w`.
   ============================================================ */
:root { --cell-w: 64px; }

/* Numeric cells: one width site-wide. `!important` is here on purpose -
   the three legacy inline widths (52/58/64px) are attribute styles on
   the element and outrank any class or element rule. They are being
   removed from the JS renderers in the same change; this keeps the
   token authoritative for anything rendered before that lands and for
   any markup added later. */
.data-table input[type="number"],
table.combo-grid input.c-fac {
    width: var(--cell-w) !important;
}

/* A fixed-width control centred in whatever the column ends up being,
   so widening a table does not leave the box hard against the left
   cell edge. Cells are already text-align:center from the pass above;
   this states it for the controls themselves. */
.data-table input[type="number"] {
    display: inline-block;
    text-align: center;
}

/* ---- Free-text cells: fill their own cell, and read left --------------
   CORRECTION, 2026-10-08. The first cut of this pass put text inputs on
   the numeric token as well, at 170px. That was wrong twice over and the
   owner saw it in the Loads panel:

   1. `#loads-table` is `table-layout: fixed` with `.c-desc` pinned at
      132px. A 170px input inside a 132px cell overflows its column by
      ~50px and paints over the adjacent Force select - which is exactly
      the "the textbox connects up to the dropbox" report. The token
      belongs to NUMBERS only.
   2. It also hit a cell nobody was looking at: `.c-target` (line 2505 in
      core.js) is a text input too, holding "ALL, 1 TO 10, 4,5, 7 TO 10"
      in a 108px column. At 64px that string was unreadable.

   So: numbers take the token, text fills its cell. A text input is
   sized by the column the column was designed around, and a px width on
   it is a second, conflicting size source.

   `!important` here beats `#loads-table input[type="text"]{text-align:
   center}` and panel.css's `#loads-table td input`, both of which are id-
   qualified and would otherwise centre this regardless of order. */
/* 🔴 THE `!important` IS STILL NEEDED FOR THE WIDTH, AND NOW ALSO CARRIES THE
   ALIGNMENT (owner, 2026-10-13: centre everything in the Loads table). It beats
   `#loads-table input[type="text"]` above and panel.css's `#loads-table td
   input`, both of which are id-qualified and would otherwise re-centre — or
   left-align — this regardless of order.

   It was `text-align: left !important` and it is now `center !important`. The
   long note above this block explains why the rule exists at all (a text input
   must fill its own cell rather than take a px width, or it overflows a
   fixed-layout column); that argument is untouched. Only the alignment changed. */
.data-table input[type="text"] {
    width: 100% !important;
    text-align: center !important;
    /* The extra pixel of left padding gives the cursor a little air off
       the border; the right side keeps the cell padding so the box never
       touches the next column's control. */
    padding-left: 7px;
}

/* The one text input that is NOT sized by its cell: the combination name
   column in the Combos editor. combo-grid is auto-layout, and the column
   has to hold the longest generated name (sizeComboNameColumn). */
table.combo-grid input.c-cname {
    width: 170px !important;
}

/* The supports table's tick cells are glyphs, not values - they were
   already a deliberate 46px, and 64px would leave six near-empty
   boxes. Held apart from the token on purpose, not an omission. */
#supports-table select {
    width: auto;
    min-width: 46px;
}


/* ============================================================
   L/C columns: the row-height problem [#table-consistency]
   ------------------------------------------------------------
   The L/C cell holds "<combo number> <combo name>", e.g.
   "7 ULS-Gravity (1.2DL+1.5LL)". In a table with 9 columns of fixed
   `width:100%`, that string was the one with no intrinsic minimum,
   so it got the smallest share of the width - and then WRAPPED onto
   two lines. Every row grew to two lines tall, which is the whole
   symptom: not a truncated column, a table whose rows are double
   height for no reason the user can see.

   The fix is to stop the cell wrapping, not to give it more width:
   the table already scrolls horizontally (main.css:990 makes the
   tab content the scroller), so a long L/C reads by scrolling rather
   than by doubling every row.
   ============================================================ */
.data-table th.lc,
.data-table td.lc {
    white-space: nowrap;
    text-align: left;
}
/* A floor so a one-word case name still gets a column, not a sliver.
   92px fits "12 ULS-4" un-wrapped at the 11px mono body size. */
.data-table th.lc,
.disp-extrema td.lc,
.rel-disp td.lc {
    min-width: 92px;
}

/* NOT applied to the Forces station table (.data-table.forces). That
   table virtualises: every row carries an explicit `height:${ROW_H}px`
   and the windowing math in core.js assumes a uniform row height to
   compute which blocks are on screen. A nowrap L/C there is right in
   principle and wrong in practice - the cell already has a rowspan
   and `vertical-align:top`, so it cannot grow its row; it simply
   overflows. Left alone deliberately. */





