.right-menu {
    position: fixed;
    right: 0;
    top: 70px;
    width: 30vw;
    z-index: 4000;
    height: calc(100vh - 70px - 59px);
}

.structure-select {
    background-color: var(--t-surface, #fff);
}

.search-table-cell-max-width {
    --max-width: 300px;
    max-width: var(--max-width);
    min-width: var(--max-width);
    width: var(--max-width);
}
.no-border-right {
    border-right: none;
}

.custom-tooltip .tooltip-inner {
    max-width: none; /* Allow dynamic width */
    white-space: normal; /* Allow wrapping */
    text-align: left;
    cursor: text; /* Enable text selection */
    padding: 10px;
}

/* Tooltip content container */
.tooltip-content {
    overflow: auto;
    resize: horizontal;
    min-width: 150px;
    max-width: 600px;
    border: 1px solid var(--t-line, #ccc);
    position: relative;
    padding: 10px;
    display: flex;
    align-items: center;
}

/* Text inside tooltip */
.tooltip-text {
    font-family: monospace;
    user-select: text;
    margin: 0;
    flex-grow: 1; /* Allows content to expand */
}

.no-border-top {
    border-top: none;
}

.no-border-bottom {
    border-bottom: none !important;
}

.light-border {
    border: 1px solid var(--bs-gray-400);
}

.light-border-bottom {
    border-bottom: 1px solid var(--bs-gray-400);
}

.light-border-top {
    border-top: 1px solid var(--bs-gray-400);
}
.search-table {
    z-index: 1;
    overflow: auto;
}
.search-table table {
    border-collapse: separate;
    border-spacing: 0;
}

.search-table thead {
    position: sticky;
    top: -1px;
    background-color: var(--t-surface-2, #f8f9fa);
    z-index: 1;
}

.search-table thead th button {
    font-size: 1.5em;
}

.search-table-cell-min-width {
    --min-width: 300px;
    min-width: var(--min-width);
}

.quickfilter-searchbar {
    font-size: 1em;
}

.quickfilter-searchbar.dropdown-menu .dropdown-item:nth-child(1):active,
.quickfilter-searchbar.dropdown-menu .dropdown-item:last-child:active {
    background-color: var(--bs-body-bg);
}

/* ── search bar (SearchBar in search.js) ─────────────────────────────────────
   One field: title addon, input, clear, options and search. It used to look like four pills because
   the buttons sat in a d-flex wrapper inside the input group - not its children, so Bootstrap's
   input-group corner and border rules never reached them, and each .btn kept its own border and
   the shell's 6px radius.

   The group draws the one outline and the rounded corners and clips its segments to them, so no
   segment needs a radius of its own and the focus ring can go round the whole thing. Segments have
   no border; a divider is a left border. Bootstrap still pulls every segment after the first 1px
   to the left (.input-group > :not(:first-child)), which lays each divider on its neighbour's last
   pixel, and on a phone, where the title is hidden, clips the input's divider away at the start. */
.search-bar {
    --search-bar-line: var(--sl-200, #ced4da);
    --search-bar-radius: var(--shell-radius, 0.25rem);
    min-width: 0;
}

.search-bar__anchor {
    position: relative;
}

.search-bar .search-bar__group {
    flex-wrap: nowrap;
    overflow: hidden;
    background-color: var(--bs-body-bg, #fff);
    border: 1px solid var(--search-bar-line);
    border-radius: var(--search-bar-radius);
    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}

.search-bar .search-bar__group:focus-within {
    border-color: var(--brand-500, #ff7b1f);
    box-shadow: 0 0 0 3px rgba(var(--brand-rgb, var(--bs-primary-rgb, 255, 105, 0)), 0.12);
}

.search-bar .search-bar__group > .search-bar__title,
.search-bar .search-bar__group > .search-bar__input,
.search-bar .search-bar__group > .btn {
    border: 0;
    border-radius: 0;
    box-shadow: none;
}

.search-bar .search-bar__group > .search-bar__title {
    color: var(--sl-600, #495057);
    background-color: var(--sl-100, #e9ecef);
    font-size: inherit;
}

.search-bar .search-bar__group > .search-bar__input,
.search-bar .search-bar__group > .search-bar__options,
.search-bar .search-bar__group > .search-bar__submit {
    border-left: 1px solid var(--search-bar-line);
}

.search-bar .search-bar__group > .search-bar__input:focus {
    border-color: var(--search-bar-line);
    box-shadow: none;
}

.search-bar .search-bar__group > .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-width: 34px;
    line-height: 1;
}

.search-bar .search-bar__group > .btn .mdi {
    font-size: 16px;
    line-height: 1;
}

/* Sits in the input's white, no divider: part of the text field rather than a segment of its own. */
.search-bar .search-bar__group > .search-bar__clear {
    padding: 0 6px;
    color: var(--sl-500, #6c757d);
    background-color: transparent;
}

.search-bar .search-bar__group > .search-bar__clear:hover {
    color: var(--sl-800, #212529);
}

.search-bar .search-bar__group > .search-bar__options {
    padding: 0 10px;
    color: var(--sl-600, #495057);
    background-color: var(--bs-body-bg, #fff);
}

.search-bar .search-bar__group > .search-bar__options:hover,
.search-bar .search-bar__group--open > .search-bar__options {
    color: var(--sl-800, #212529);
    background-color: var(--sl-50, #f8f9fa);
}

.search-bar .search-bar__group > .search-bar__submit {
    padding: 0 16px;
    border-left-color: var(--brand-600, var(--bs-primary, #ff6900));
}

.search-bar .search-bar__group > .btn:focus-visible {
    outline: 0;
    box-shadow: inset 0 0 0 2px rgba(var(--brand-rgb, var(--bs-primary-rgb, 255, 105, 0)), 0.55);
}

.search-bar .search-bar__group > .search-bar__submit:focus-visible {
    box-shadow: inset 0 0 0 2px var(--brand-800, #a84500);
}

.search-bar__submit-icon {
    display: none;
}

/* A phone: the title is already gone (d-lg-flex), the search button keeps its name for assistive
   technology but shows only the magnifier, so the input keeps as much of the width as it can. */
@media (max-width: 575.98px) {
    .search-bar .search-bar__group > .search-bar__submit {
        padding: 0 10px;
    }

    .search-bar__submit-icon {
        display: inline-block;
    }

    .search-bar__submit-text {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
    }
}

.search-pagination-button {
    font-size: 1.5rem;
    padding: 0;
}

.search-pagination-page-button {
    padding: 2px;
}

.search-table {
    overflow-y: auto;
    overflow-x: auto;
    max-width: 100%;
}

.search-table table thead th {
    position: relative;
}

.search-pagination-page-button {
    font-size: 1.0rem;
    margin-left: 2px;
    margin-right: 2px;
    height: 40px;
    width: 40px;
    white-space: nowrap;
}

.search-button-bar-left {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    flex-wrap: wrap;
    width: 50%;
}

/* ── the checkbox selection: the "N of M selected" chip and the select-all banner ─────────
   The chip sits with the bulk menus, because it says what those menus will act on. The banner sits
   directly above the table, because the click it answers was made in the table's own header row.
   Both are drawn by SearchTable (search.js); see search-behaviours.js for what is actually stored. */

/* Coloured here rather than with `bg-primary-subtle text-primary-emphasis`: those utilities are
   Bootstrap 5.3 and the theme is 5.1, so the chip would have come out as unstyled text. */
.search-selection-counter__chip {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    border-radius: 999px;
    padding: 0.2rem 0.35rem 0.2rem 0.6rem;
    font-size: 0.78rem;
    font-weight: 600;
    line-height: 1.5;
    color: rgb(var(--bs-primary-rgb, 13, 110, 253));
    background-color: rgba(var(--bs-primary-rgb, 13, 110, 253), 0.12);
}

.search-selection-counter__clear {
    display: inline-flex;
    align-items: center;
    font-size: 0.95rem;
    line-height: 1;
    color: inherit;
    opacity: 0.75;
}

.search-selection-counter__clear:hover,
.search-selection-counter__clear:focus-visible {
    opacity: 1;
}

/* The live region stays in the document at all times, so that a screen reader announces the change
   into an existing region instead of the region itself appearing with the text already in it - the
   latter is announced inconsistently. It carries no spacing of its own, so an empty one is invisible.
   .search-selection-live is the region; the banner inside it is what comes and goes. */

.search-selection-banner,
.search-selection-notice {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    padding: 0.4rem 0.75rem;
    margin-bottom: 0.5rem;
    border: 1px solid rgba(var(--bs-primary-rgb), 0.25);
    border-radius: var(--bs-border-radius, 0.25rem);
    background-color: rgba(var(--bs-primary-rgb), 0.07);
    font-size: 0.85rem;
    text-align: center;
}

.search-selection-notice {
    border-color: var(--bs-gray-300, #dee2e6);
    background-color: var(--bs-gray-100, #f8f9fa);
    color: var(--bs-secondary-color, #6c757d);
}

.search-selection-banner__action {
    padding: 0;
    font-size: 0.85rem;
    font-weight: 600;
    text-decoration: underline;
}

.search-button-bar-right {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    width: 50%;
}

/* ── the search page on the dense shell layout ───────────────────────────
   search/searchv2.html sets shellDense, and the shell takes its gutters down to 6px (shell.css, "the
   dense layout"). These take the page's own spacing down with it: the filter sidebar's and the result
   table's 8px outer padding goes, the table card's inner padding is 8px instead of 16px, and the gap
   under the search bar is 6px instead of 16px - so the sidebar, the table and the gutters are all 6px
   apart. The utility classes they replace are Bootstrap's, which are !important.

   Only #searchTable, the search page's own table: a search table embedded in a datasheet tab
   (#searchTable-<id>) keeps the spacing its template gives it. The table's height on this layout is
   search.js denseHeight: the window less the topbar, the footer and the container's gutter, measured -
   so nothing here may add vertical space outside the table's own box. */
body.shell-dense #searchTable .search-page__menu {
    padding: 0 var(--shell-pad, 6px) 0 0 !important;
}

body.shell-dense #searchTable .search-page__menu.w-100 {
    padding: 0 !important;
}

body.shell-dense #searchTable .search-page__main {
    padding: 0 !important;
}

/* col-md-3 is a quarter of the row: 420px of filter list next to the table on a 1920px screen. From 1200px the
   sidebar is a fifth, between 240 and 340px, and the table takes the rest. Below that the grid's widths stay. */
@media (min-width: 1200px) {
    body.shell-dense #searchTable .search-page__menu:not(.w-100) {
        flex: 0 0 clamp(240px, 20%, 340px);
        width: clamp(240px, 20%, 340px);
        max-width: none;
    }

    body.shell-dense #searchTable .search-page__menu:not(.w-100) + .search-page__main {
        flex: 1 1 0;
        width: auto;
        max-width: none;
        /* A flex item will not shrink below its content, and fourteen columns are wider than the screen:
           without this the table wraps under the sidebar instead of scrolling inside its own box. */
        min-width: 0;
    }
}

body.shell-dense #searchTable .search-page__table {
    padding: 8px !important;
}

body.shell-dense #searchTable .search-table__gap {
    margin-bottom: 6px !important;
}

.input-highlight-primary-soft {
    background-color: rgba(var(--bs-primary-rgb), 0.15); /* Soft, light version of primary */
    border-color: var(--bs-primary);                     /* Primary border color */
    color: var(--bs-primary);                            /* Optional: Primary text color */
}

.loader {
    width: 48px;
    height: 48px;
    border: 5px solid var(--bs-primary);
    border-bottom-color: transparent;
    border-radius: 50%;
    display: inline-block;
    box-sizing: border-box;
    animation: rotation 1s linear infinite;
}

@keyframes rotation {
    0% {
        transform: rotate(0deg);
    }
    100% {
        transform: rotate(360deg);
    }
}

.right-panel {
    position: fixed;
    right: 0;
    top: 0;
    bottom: 0;
    background-color: var(--t-surface, #fff);
    box-shadow: -2px 0 5px rgba(0, 0, 0, 0.3);
    overflow-y: auto;
    z-index: 10000; /* Ensures it overlaps other elements */
    display: none;
}

.resizer {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 12px;
    cursor: ew-resize;
    display: flex;
    align-items: center;
    justify-content: center;
}
.search-table .vue-table {
    display: grid;
    grid-template-columns: var(--grid-cell-sizes);
    gap: 0; /* No gap between items to mimic table borders */
    border-collapse: collapse; /* Mimic Bootstrap's table look */
}

.search-table .vue-table .table-cell, .vue-table .table-header {
    padding: 10px;
    text-align: left;
    border-bottom: 1px solid var(--t-line, #dee2e6); /* Row separator */;
}

.search-table.tableSearch .vue-table .table-cell,
.search-table.tableSearch .vue-table .table-header {
    padding: 10px 18px;
}

.box-shadow-right {
    box-shadow: 5px 0px 5px rgba(0, 0, 0, 0.2);
}

.box-shadow-left {
    box-shadow: -5px 0px 5px rgba(0, 0, 0, 0.2);
}

.resizer:hover {
    border-left: 2px solid var(--bs-primary);
}

.resizer-cell {
    position: absolute;
    top: 0;
    right: 0;
    width: 5px;
    height: 100%;
    cursor: col-resize;
    background-color: transparent;
}

/* Hover effect for resizer */
.table-header:hover .resizer-cell {
    background-color: var(--t-fill, #ddd);
}

.entry-info {
    font-family: 'Arial', sans-serif;
    font-size: 14px;
    /* --bs-light-primary is defined by no stylesheet this application ships (the theme is a Bootstrap 5.1
       build), so without a fallback this declaration is dropped in full and the text takes what it inherits. */
    color: var(--bs-light-primary, #ff6900);
    padding: 8px 0;
}

.entry-info span {
    display: inline-block;
}

.entry-info strong {
    color: var(--bs-primary); /* Modern blue accent */
    font-weight: 600;
}

.resizer::before {
    content: '⋮⋮'; /* Vertical dots symbol */
    font-size: 1rem;
    color: rgba(0, 0, 0, 0.8);
    font-weight: bold;
}

.page-content {
    margin-top: 0px !important;
}

.right-panel.active {
    display: block; /* Show the panel when clicked */
}

.property-panel {
    padding: 16px;
}
.property-header {
    font-weight: bold;
    color: var(--t-ink-strong, #333);
}
.property-item {
    background-color: var(--bs-gray-100);;
    border: 1px solid var(--bs-gray-200);
    border-radius: 0px;
    padding: 8px 12px;
    margin-bottom: 8px;
}

.property-item.active-value {
    border-color: var(--bs-primary);;
}

.property-item-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.icon-button {
    background: none;
    border: none;
    cursor: pointer;
    color: var(--t-ink-muted, #6c757d);
}
.icon-button:hover {
    color: red;
}
.add-property {
    color: var(--bs-primary);
    font-weight: 500;
    cursor: pointer;
}
.collapse-button {
    color: var(--t-ink-strong, #333);
    text-decoration: none;
    font-size: 1rem;
}

div.table-cell {
    z-index: 0;
}

div.table-header {
    position: sticky !important;
    z-index: 1;
    top: 0;
    text-align: center;
    font-weight: bold;
    background-color: var(--bs-gray-200); /* Light gray background */
}

/*
 * Heading on the left, buttons on the right, on one line - which is where they were before, and
 * where they read best when there is room for both.
 *
 * They were stacked for a while because a narrow column spent its whole width on the buttons and
 * the heading went first to "So..." and then to nothing at all. Stacking fixed that and cost every
 * wide column 20px of height for a problem only the narrow ones had. A narrow column now folds its
 * buttons into one menu instead, so nothing is stacked and nothing is anonymous - see
 * .table-header__more below.
 */
.vue-table .table-header .table-header__layout {
    gap: 0.25rem;
}

/* Free to shrink to nothing, because the buttons stop competing with it before that happens. */
.vue-table .table-header .table-header__name {
    flex: 1 1 auto;
    min-width: 0;
}

.vue-table .table-header .table-header__actions {
    flex: 0 0 auto;
    margin-left: auto;
}

/*
 * The folded buttons: one control, and the menu it opens.
 *
 * Positioned relative to the header rather than the table so the menu opens under the column it
 * belongs to, and above the sticky headers' own z-index so it is not clipped by the next column.
 */
.vue-table .table-header .table-header__more {
    position: relative;
}

/*
 * What a folded header still says about itself.
 *
 * Smaller than a button and not one: the header already sorts when clicked, so these would be a
 * second control in the same place doing the same thing. They are the colour an active state is
 * drawn in everywhere else in this table, so a sorted column reads as sorted at a glance.
 */
.vue-table .table-header .table-header__more {
    display: flex;
    align-items: center;
    gap: 0.15rem;
}

.vue-table .table-header .table-header__state {
    font-size: 0.9rem;
    line-height: 1;
    color: var(--bs-primary);
    pointer-events: none;
}

.vue-table .table-header .table-header__menu {
    position: absolute;
    top: 100%;
    right: 0;
    z-index: 10;
    min-width: 11rem;
    padding: 0.25rem 0;
    background-color: var(--bs-body-bg, #fff);
    border: 1px solid var(--bs-border-color, var(--bs-gray-300));
    border-radius: var(--parameter-band-radius, 0.25rem);
    box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
    text-align: left;
}

.vue-table .table-header .table-header__menu-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0.35rem 0.75rem;
    border: 0;
    background: none;
    /* The header is bold and centred; a menu is a list of actions and reads as one. */
    font-weight: 400;
    text-align: left;
    white-space: nowrap;
    color: var(--bs-body-color);
}

.vue-table .table-header .table-header__menu-item:hover,
.vue-table .table-header .table-header__menu-item:focus-visible {
    background-color: var(--bs-gray-100);
}

.footer {
    position: fixed;
}

div.sticky-start {
    position: sticky !important;
    left: calc(var(--sticky-start-left) - 1px); /* Sticky for horizontal scrolling */
}

.table-header.sticky-start {
    z-index: 3; /* Higher z-index to keep headers above the content */
    top: 0px;
}

.table-cell.sticky-start {
    z-index: 2; /* Higher z-index to keep headers above the content */
    /* Frozen cells must be opaque and match the table surface, not the darker page ground. */
    background: var(--t-surface, #fff) !important;
}

.table-header.sticky-end {
    z-index: 3; /* Higher z-index to keep headers above the content */
    top: 0px;
    border-left: 1px solid var(--bs-gray-400);
}

.table-cell.sticky-end {
    z-index: 2; /* Higher z-index to keep headers above the content */
    background: var(--t-surface, #fff) !important;
}

.daterangepicker {
    z-index: 99999;
}

.breadcrumb-badge {
    background-color: var(--color-background);
    border: 1px solid var(--bs-primary);
    border-radius: 6px;
    padding: 2px 4px;
    white-space: nowrap;
}

.mdi-close {
    color: var(--bs-primary);
}

div.sticky-end {
    position: sticky !important;
    right: 0;
}

td.sticky-start {
    position: sticky;
    left: calc(var(--sticky-start-left) - 1px); /* Sticky for horizontal scrolling */
}

td.sticky-end {
    position: sticky;
    right: 0;
}

.select2-results__option--selected {
    background-color: #f0f8ff; /* Light blue highlight */
    color: var(--t-ink-strong, #000); /* Black text for better contrast */;
    font-weight: bold;
}

.select2-selection__choice {
    text-wrap: auto;
}
.table-header .sort.active {
    color: var(--bs-primary);
}

.truncate-text {
    white-space: nowrap; /* Prevent line breaks */
    overflow: hidden; /* Hide overflow content */
    text-overflow: ellipsis; /* Add "..." to clipped text */
    display: block; /* Ensure the element behaves as a block or inline-block */
}

.table-header i {
    color: var(--bs-body-color);
}

.table-header i:hover {
    box-shadow: var(--bs-body-color) 0px 0px 5px;
}

.explanation-content {
    font-weight: bolder;
    font-style: italic;
}

.vs__selected {
    background-color: var(--bs-soft-primary);
    color: var(--bs-body-color);
}

.vs__dropdown-option--highlight {
    background: var(--bs-soft-primary);
    color: var(--bs-body-color);
}

.structure-select .vs__dropdown-option {
    padding: 0;
}

.structure-select-option {
    padding: 0.2rem 1rem;
}

.vs--searchable .vs__dropdown-toggle {
    cursor: pointer;
}

/* ---------------------------------------------------------------------------
 * Parameter filters
 *
 * Parameters qualify a datasheet line (measured at 23 °C, per EN 10025, on sheet)
 * rather than describing the material, and filtering on one changes which values
 * you are shown, not just which rows. That is a different kind of act from a
 * normal column filter, so parameter columns and their widgets carry their own
 * accent colour to make the distinction legible at a glance.
 *
 * The accent is defined once here and derived from the Bootstrap theme, so it
 * follows light/dark mode with everything else.
 * ------------------------------------------------------------------------- */
:root {
    --parameter-accent: #7952b3;
    --parameter-accent-soft: rgba(121, 82, 179, 0.12);
    --parameter-accent-border: rgba(121, 82, 179, 0.35);
}

[data-bs-theme="dark"] {
    --parameter-accent: #b794f6;
    --parameter-accent-soft: rgba(183, 148, 246, 0.16);
    --parameter-accent-border: rgba(183, 148, 246, 0.4);
}

/* Column header of a parameter, in the table and in the settings list. */
.parameter-column {
    background-color: var(--parameter-accent-soft);
    box-shadow: inset 0 -2px 0 var(--parameter-accent);
}

.parameter-column .mdi-filter,
.parameter-column .mdi-filter-outline {
    color: var(--parameter-accent) !important;
}

/* Marker shown next to a parameter's name wherever fields are listed. */
.parameter-marker {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    color: var(--parameter-accent);
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    text-transform: uppercase;
}

.parameter-badge {
    background-color: var(--parameter-accent-soft);
    color: var(--parameter-accent);
    border: 1px solid var(--parameter-accent-border);
    font-weight: 600;
}

/*
 * The bounds are also a way in: the step is chosen for the span, so an exact value is often
 * unreachable by dragging, and clicking the number types it instead. It has to keep looking like the
 * badge it replaces, so the button base is stripped rather than restyled.
 */
.parameter-badge--editable {
    cursor: text;
    font: inherit;
    line-height: inherit;
}

.parameter-badge--editable:hover,
.parameter-badge--editable:focus-visible {
    background-color: var(--parameter-accent);
    color: var(--bs-body-bg);
}

/* Sized to the badge it stands in for, so opening the editor does not reflow the row around it. */
.parameter-badge__input {
    width: 6rem;
    text-align: center;
}

.parameter-filter {
    min-width: 18rem;
}

/* --- two-handle slider --------------------------------------------------- */
.parameter-slider {
    position: relative;
    height: 2.25rem;
}

.parameter-slider__rail,
.parameter-slider__track {
    position: absolute;
    top: 50%;
    height: 0.35rem;
    border-radius: 999px;
    transform: translateY(-50%);
    pointer-events: none;
}

.parameter-slider__rail {
    left: 0;
    right: 0;
    background-color: var(--bs-border-color, #dee2e6);
}

.parameter-slider__track {
    background-color: var(--parameter-accent);
    opacity: 0.45;
}

.parameter-slider--active .parameter-slider__track {
    opacity: 1;
}

/*
 * Both range inputs are stacked over the same rail. The element itself must not
 * swallow pointer events or the upper handle would be unreachable wherever the
 * two overlap - only the thumbs take pointer events, re-enabled below.
 */
.parameter-slider__input {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    background: none;
    appearance: none;
    -webkit-appearance: none;
    pointer-events: none;
}

.parameter-slider__input::-webkit-slider-thumb {
    -webkit-appearance: none;
    pointer-events: auto;
    width: 1.1rem;
    height: 1.1rem;
    border-radius: 50%;
    background-color: var(--parameter-accent);
    border: 2px solid var(--bs-body-bg, #fff);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
    cursor: pointer;
}

.parameter-slider__input::-moz-range-thumb {
    pointer-events: auto;
    width: 1.1rem;
    height: 1.1rem;
    border-radius: 50%;
    background-color: var(--parameter-accent);
    border: 2px solid var(--bs-body-bg, #fff);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
    cursor: pointer;
}

.parameter-slider__input::-moz-range-track {
    background: none;
}

.parameter-slider__input:focus-visible::-webkit-slider-thumb {
    outline: 2px solid var(--parameter-accent);
    outline-offset: 2px;
}

/* --- multi-select -------------------------------------------------------- */
.parameter-option-list {
    max-height: 15rem;
    overflow-y: auto;
    border: 1px solid var(--bs-border-color, #dee2e6);
    border-radius: 0.375rem;
}

.parameter-option {
    display: flex;
    align-items: center;
    padding: 0.35rem 0.6rem;
    margin: 0;
    cursor: pointer;
    border-bottom: 1px solid var(--bs-border-color-translucent, rgba(0, 0, 0, 0.05));
}

.parameter-option:last-child {
    border-bottom: none;
}

.parameter-option:hover {
    background-color: var(--parameter-accent-soft);
}

.parameter-option--selected {
    background-color: var(--parameter-accent-soft);
    font-weight: 600;
}

/* ---------------------------------------------------------------------------
 * Parameter groups - one row per material, condition sets folded underneath
 *
 * A material measured under four condition sets used to produce four result
 * rows. It now produces one parent row plus one sub-row per condition set, and
 * the group is tinted so you can see where it starts and ends while scrolling.
 *
 * Colour split, deliberately: the band is the Bootstrap *primary* family, since
 * it marks structure (this is one result, these are its variants) exactly like
 * the rest of the app's selection tinting. The purple --parameter-accent stays
 * reserved for parameter-ness itself - column headers, markers, chips - so a
 * tinted group is never mistaken for a filtered parameter column.
 *
 * --bs-soft-primary is a solid mid-tone (#FFB47F against the default primary)
 * and is far too loud behind a full result row, so the band tints are derived
 * from rgba(var(--bs-primary-rgb), ...) the way .input-highlight-primary-soft
 * already does. Alpha over the body background keeps them theme-agnostic; dark
 * mode only needs the alphas nudged up to stay visible.
 *
 * Structure, and why nothing below mentions a row: .vue-table is a CSS grid of
 * flat .table-cell siblings (see TableBody) - there is no row element at all.
 * A "row" is therefore a convention: every cell of a material carries
 * .parameter-parent-cell, the outermost two add .is-row-start / .is-row-end,
 * and state that would otherwise be :hover or a class on a <tr> arrives as
 * .is-hovered / .is-expanded on every cell of that row.
 * ------------------------------------------------------------------------- */
:root {
    --parameter-band-bg: rgba(var(--bs-primary-rgb), 0.06);
    --parameter-band-bg-hover: rgba(var(--bs-primary-rgb), 0.13);
    --parameter-band-bg-expanded: rgba(var(--bs-primary-rgb), 0.10);
    --parameter-band-sub-bg: rgba(var(--bs-primary-rgb), 0.03);
    --parameter-band-edge: var(--bs-primary);
    --parameter-band-edge-soft: rgba(var(--bs-primary-rgb), 0.35);
    --parameter-band-rule: rgba(var(--bs-primary-rgb), 0.18);
    --parameter-band-close: rgba(var(--bs-primary-rgb), 0.45);
    --parameter-band-indent: 2.25rem;
    /*
     * Fixed rather than content-derived, because the sticky header below the bands has to be offset
     * by exactly this much and CSS cannot ask an element how tall its neighbour turned out. The band
     * holds one line of 0.7rem uppercase text, so 1.5rem leaves it room at any zoom.
     */
    --parameter-band-height: 1.5rem;
    --parameter-band-radius: 0.25rem;
    --parameter-chip-muted-bg: var(--bs-secondary-bg, var(--bs-gray-200));
    --parameter-chip-muted-color: var(--bs-secondary-color, var(--bs-gray-600));
    --parameter-chip-muted-border: var(--bs-border-color, var(--bs-gray-300));
}

[data-bs-theme="dark"] {
    --parameter-band-bg: rgba(var(--bs-primary-rgb), 0.12);
    --parameter-band-bg-hover: rgba(var(--bs-primary-rgb), 0.22);
    --parameter-band-bg-expanded: rgba(var(--bs-primary-rgb), 0.18);
    --parameter-band-sub-bg: rgba(var(--bs-primary-rgb), 0.07);
    --parameter-band-rule: rgba(var(--bs-primary-rgb), 0.28);
    --parameter-band-close: rgba(var(--bs-primary-rgb), 0.55);
    --parameter-chip-muted-bg: var(--bs-secondary-bg, var(--bs-gray-800));
    --parameter-chip-muted-color: var(--bs-secondary-color, var(--bs-gray-400));
    --parameter-chip-muted-border: var(--bs-border-color, var(--bs-gray-700));
}

/* --- parent row ---------------------------------------------------------- */
/*
 * The tint has to be painted on every cell, because there is nothing behind the
 * cells to paint instead. Which tint is in play is kept in one custom property
 * so the state rules below only ever swap a value - the actual background is
 * declared once, and no state can accidentally leave a cell of the row a
 * different colour from its neighbours.
 *
 * Nothing here adds horizontal spacing, borders or per-cell rounding: the grid
 * runs at gap 0, so the tints of adjacent cells butt together into one band.
 * Anything that separated them would immediately read as stripes.
 */
.parameter-parent-cell {
    --parameter-band-tint: var(--parameter-band-bg);
    background-color: var(--parameter-band-tint);
    cursor: pointer;
}

/* An open group has to stay obvious once its first cells scroll past the fold. */
.parameter-parent-cell.is-expanded {
    --parameter-band-tint: var(--parameter-band-bg-expanded);
}

/*
 * A grid has no row box to :hover, so the markup puts .is-hovered on every cell
 * of the hovered row. Declared after .is-expanded so that hovering an open
 * group still lifts it - equal specificity, later wins.
 */
.parameter-parent-cell.is-hovered {
    --parameter-band-tint: var(--parameter-band-bg-hover);
}

/*
 * Sticky columns are opaque (see .table-cell.sticky-*) so that body text cannot
 * scroll through them. That would punch a hole in the band, so re-state it here
 * as an opaque body-coloured base plus the tint as a gradient layer - the same
 * composite the translucent tint produces over the body background elsewhere in
 * the row. (Those rules said "background: white" when this comment was written;
 * they read --bs-body-bg now, for the reason the rest of this paragraph gives.)
 */
.search-table .vue-table .table-cell.parameter-parent-cell.sticky-start,
.search-table .vue-table .table-cell.parameter-parent-cell.sticky-end {
    background-color: var(--bs-body-bg, #fff) !important;
    background-image: linear-gradient(var(--parameter-band-tint), var(--parameter-band-tint)) !important;
}

/*
 * The accent edge marks where the group begins, so it belongs on the first cell
 * only. It is an inset shadow rather than a border because the grid tracks are
 * sized from --grid-cell-sizes: a real border would eat into the first column's
 * content box and shift its text out of line with every non-grouped row, and
 * thickening it on expand would make the text jump.
 */
.parameter-parent-cell.is-row-start {
    box-shadow: inset 3px 0 0 var(--parameter-band-edge);
    border-top-left-radius: var(--parameter-band-radius);
    border-bottom-left-radius: var(--parameter-band-radius);
}

.parameter-parent-cell.is-row-end {
    border-top-right-radius: var(--parameter-band-radius);
    border-bottom-right-radius: var(--parameter-band-radius);
}

.parameter-parent-cell.is-row-start.is-expanded {
    box-shadow: inset 4px 0 0 var(--parameter-band-edge);
}

/*
 * .box-shadow-right is added to a sticky-start cell once the table is scrolled;
 * it is the same property as the accent edge, so the two have to be written as
 * one declaration or the last one to load silently wins.
 */
.parameter-parent-cell.is-row-start.box-shadow-right {
    box-shadow: inset 3px 0 0 var(--parameter-band-edge), 5px 0 5px rgba(0, 0, 0, 0.2);
}

.parameter-parent-cell.is-row-start.is-expanded.box-shadow-right {
    box-shadow: inset 4px 0 0 var(--parameter-band-edge), 5px 0 5px rgba(0, 0, 0, 0.2);
}

/*
 * Open: the band continues downwards into .parameter-sub-rows, so the corners
 * that face it lose their rounding and the row separator is suppressed. The
 * separator is declared by ".search-table .vue-table .table-cell", which needs
 * matching in kind before it will give way.
 */
.parameter-parent-cell.is-expanded.is-row-start {
    border-bottom-left-radius: 0;
}

.parameter-parent-cell.is-expanded.is-row-end {
    border-bottom-right-radius: 0;
}

.search-table .vue-table .table-cell.parameter-parent-cell.is-expanded {
    border-bottom-color: transparent;
}

/* --- disclosure triangle ------------------------------------------------- */
.parameter-twist {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;   /* 24px - minimum comfortable pointer target */
    height: 1.5rem;
    padding: 0;
    margin-right: 0.25rem;
    border: none;
    background: none;
    border-radius: 0.25rem;
    color: var(--bs-primary);
    line-height: 1;
    vertical-align: middle;
    cursor: pointer;
    transition: transform 0.15s ease-in-out;
}

.parameter-twist:hover {
    color: var(--bs-primary);
    background-color: rgba(var(--bs-primary-rgb), 0.15);
}

/*
 * :focus-visible only, so a mouse click does not leave a ring behind, but
 * keyboard users always see where they are.
 */
.parameter-twist:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

/*
 * Rotating the button rotates whatever glyph it wraps, icon font or not. The
 * second selector is a safety net: the surrounding cell is marked open anyway,
 * so the triangle points the right way even if the button misses the class.
 */
.parameter-twist.is-expanded,
.parameter-parent-cell.is-expanded .parameter-twist {
    transform: rotate(90deg);
}

@media (prefers-reduced-motion: reduce) {
    .parameter-twist {
        transition: none;
    }
}

/* --- detail region ------------------------------------------------------- */
/*
 * One element follows the row's cells and spans every column, the same trick
 * the empty state uses. Spanning all tracks is what keeps the columns intact:
 * the element is placed on its own grid line, so the next material's cells
 * still start against column 1.
 *
 * "contain: inline-size" is the part that must not be removed. The track list
 * from getTableCss() is full of "auto" and "minmax(auto, ...)" entries, and an
 * item spanning intrinsic tracks has its max-content width distributed back
 * over them - a long message would quietly widen the real columns underneath.
 * Inline-size containment makes this element size as if it were empty for that
 * calculation, so its contents can never push a column around.
 *
 * Only the non-tabular states use this now: loading, failure, and "none
 * recorded". The condition sets themselves are real cells - see
 * .parameter-sub-cell below.
 */
.parameter-sub-rows {
    grid-column: 1 / -1;
    contain: inline-size;
    min-width: 0;
    background-color: var(--parameter-band-sub-bg);
    /* Indented from the left so the condition sets read as subordinate. */
    padding: 0.15rem 1rem 0.15rem var(--parameter-band-indent);
    /* The edge runs on through the group, lighter, so it reads as one block. */
    box-shadow: inset 3px 0 0 var(--parameter-band-edge-soft);
    /* Nothing on top - parent and first condition set are one band. A stronger
       rule underneath keeps consecutive groups from running together. */
    border-bottom: 2px solid var(--parameter-band-close);
    border-bottom-left-radius: var(--parameter-band-radius);
    border-bottom-right-radius: var(--parameter-band-radius);
}

/* --- a condition set, laid out under the table's own columns ------------- */
/*
 * A set is emitted as one cell per visible column, exactly like the row above
 * it, because the comparison a reader came for runs down a column: this
 * material at 20 C against the same material at 400 C. That only works if the
 * cells sit on the same grid tracks, so these are siblings of the parent row's
 * cells and never nested inside a spanning element.
 *
 * The spanning .parameter-sub-rows above is now only for loading, failure and
 * "none recorded" - states that are not tabular and have nothing to align to.
 */
.parameter-sub-cell {
    background-color: var(--parameter-band-sub-bg);
    /* Hairline above every set. The parent row's cells have none below them, so
       the parent and its first set still read as one band. */
    border-top: 1px solid var(--parameter-band-rule);
}

/* The edge runs on through the group, lighter, so it reads as one block, and the
   indent falls on the first cell only - indenting the row would drag every
   column out of line with the material rows around it. */
.parameter-sub-cell.is-row-start {
    box-shadow: inset 3px 0 0 var(--parameter-band-edge-soft);
    padding-left: var(--parameter-band-indent);
}

/*
 * The conditions that are not already columns, naming the set on a line of its own
 * across the whole grid.
 *
 * It sat in the set's first cell before, which is the narrowest column in the table:
 * with a single parameter column chosen, three conditions turned that cell into a
 * vertical wall of broken words and set the height of the entire row. Spanning the
 * grid costs a few pixels and stays readable at any column width.
 *
 * Quiet on purpose - it identifies the row, while the values in the columns are what
 * is being read - and deliberately not padded to line up with the first column,
 * because it belongs to the whole row rather than to any one cell.
 */
/*
 * The three things the condition search changes, in the dialog that explains it. Tightened
 * because the default list indent pushes them away from the sentence they belong to.
 */
.parameter-intro__points {
    margin-bottom: 0;
    padding-left: 1.1rem;
}

.parameter-intro__points li + li {
    margin-top: 0.4rem;
}

/*
 * The conditions a run of sets shares, as a heading over them.
 *
 * Printed once per group rather than once per set: an aluminium temper's description
 * runs to 96 characters, and repeating it above each of the four sets that share it
 * made the reader re-read the same sentence to find out nothing had changed.
 *
 * Built to read as a heading and not as another row - it sits tight to the rows it
 * covers, carries the accent, and is separated from the group above by space rather
 * than by a rule, so the eye groups downwards.
 */
/*
 * What every condition set of a material has in common, above all of them.
 *
 * Quieter and wider than a group heading: it is context for the whole expansion
 * rather than a divider between parts of it, so it carries no rail and no count -
 * only the group headings below it separate anything.
 */
.parameter-sub-common {
    grid-column: 1 / -1;
    padding: 0.5rem 0.75rem 0.2rem calc(0.75rem + var(--parameter-band-indent, 1.5rem));
    background-color: var(--parameter-band-sub-bg);
    color: var(--bs-secondary-color, var(--bs-gray-600));
    font-size: 0.76em;
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/*
 * A cell continuing the measurement above it rather than reporting another.
 *
 * One measurement can belong to many sets: joined on temperature, the single physical row at 20 °C
 * belongs to all eight mechanical sets at 20 °C, so its value is written into all eight. Eight
 * identical numbers down a column read as eight measurements rather than as one covering them all,
 * which is the risk p5 was chosen knowing about; p5b draws it as a spanning cell.
 *
 * The text is hidden and a rule runs down the stretch, so it reads as "the same measurement" and
 * not as "not measured" - which is what an actually empty cell here means. The value stays in the
 * cell: selecting the column still copies it, and the cell carries it as a title. A <table> would
 * say this with rowspan; this is a CSS grid, where spanning rows means placing every following cell
 * by hand and giving up auto-flow.
 *
 * Which cells get this is decided in continuesPrevious(), and the decision is the whole feature -
 * hiding a value that merely looks like the one above it hides real measurements.
 */
.table-cell.parameter-sub-cell.is-continued {
    position: relative;
    color: transparent;
}

/* Descendants too: a sub-row value is server-formatted markup, and a link or a highlighted term
   inside it sets its own colour, so hiding only the cell would leave those still showing. */
.table-cell.parameter-sub-cell.is-continued * {
    color: transparent;
}

.table-cell.parameter-sub-cell.is-continued::before {
    content: "";
    position: absolute;
    left: 10px;
    top: 0;
    bottom: 0;
    border-left: 1px solid var(--parameter-band-rule);
}

/* Selecting a column still copies the value, so a continuation is only hidden to the eye. */
.table-cell.parameter-sub-cell.is-continued::selection,
.table-cell.parameter-sub-cell.is-continued *::selection {
    color: var(--bs-body-color);
}

/* The line standing in for the sets no visible column can fill. Quiet: it reports an
   absence, and an absence should not compete with the values above it. */
.parameter-sub-empty {
    grid-column: 1 / -1;
    padding: 0.3rem 0.75rem 0.45rem calc(0.75rem + var(--parameter-band-indent, 1.5rem));
    background-color: var(--parameter-band-sub-bg);
    border-bottom: 2px solid var(--parameter-band-close);
    font-size: 0.78em;
}

.parameter-sub-group {
    grid-column: 1 / -1;
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    padding: 0.45rem 0.75rem 0.3rem calc(0.75rem + var(--parameter-band-indent, 1.5rem));
    background-color: var(--parameter-band-sub-bg);
    /* The rail ties the heading to its rows: it starts here and continues down every
       cell of the group, so the group reads as one object even when it is scrolled
       so that the heading is the only part on screen. */
    box-shadow: inset 3px 0 0 var(--parameter-band-edge-soft);
    line-height: 1.25;
}

/* Space above every heading but the first, which is what separates one group from the
   previous group's last row without spending a border on it. */
.parameter-sub-group:not(:first-of-type) {
    padding-top: 0.85rem;
}

.parameter-sub-group__label {
    color: var(--parameter-accent);
    font-weight: 600;
    font-size: 0.82em;
    letter-spacing: 0.01em;
    /* One line, clipped: a group heading must never be able to push the rows below it
       down the page. The whole text stays available as the element's title. */
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Quiet, and never the thing that gets clipped - the count is short and the label is not. */
.parameter-sub-group__count {
    flex: none;
    color: var(--bs-secondary-color, var(--bs-gray-600));
    font-size: 0.74em;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Closes the group, so a heading's reach is visible without hunting for the next one. */
.table-cell.parameter-sub-cell.is-group-end {
    border-bottom-color: var(--parameter-band-rule);
}

/* Sticky columns force an opaque white background, which would punch a hole
   through the band exactly as it does on the parent row - and be literally white
   in dark mode. Same treatment: an opaque body ground with the tint as a layer. */
.table-cell.parameter-sub-cell.sticky-start,
.table-cell.parameter-sub-cell.sticky-end {
    background-color: var(--bs-body-bg) !important;
    background-image: linear-gradient(var(--parameter-band-sub-bg), var(--parameter-band-sub-bg)) !important;
}

/*
 * Column bands: which property table a run of columns comes from, and a toggle to fold it.
 *
 * The same property is recorded by several tables - Temperature exists in Mechanical Properties, in
 * Physical Properties and as the condition the sets are keyed by - so three columns can carry
 * identical headers with nothing on screen to tell them apart. The band names the source.
 *
 * Not sticky, deliberately: the header row below is, and stacking a second sticky row means pinning
 * the header at the band's height, which is not knowable before layout. The band scrolls away and
 * the column headers stay, which is the more useful half.
 */
/*
 * The band row sticks to the top with the header it names.
 *
 * The headers below are `position: sticky; top: 0`, and the bands were static - so scrolling down
 * slid the bands away and left the columns under a header row that no longer said which property
 * table each column came from, which is the one thing the bands are for.
 *
 * Both stick, one above the other: the band at the top and the header offset by exactly the band's
 * height. That offset only applies to a header that actually FOLLOWS a band - the general sibling
 * combinator, so a table drawn without bands keeps its header at top: 0 with no second rule and no
 * :has() to depend on.
 */
.vue-table .table-band {
    position: sticky;
    top: 0;
    /* Above the plain headers and the sticky-start ones, which they never overlap but pass under. */
    z-index: 4;
    height: var(--parameter-band-height);
    box-sizing: border-box;
    line-height: 1;
    padding: 4px 10px 2px;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--bs-secondary-color, var(--bs-gray-600));
    background-color: var(--bs-gray-100);
    border-bottom: 1px solid var(--bs-gray-300);
    white-space: nowrap;
    overflow: hidden;
    display: flex;
    align-items: center;
    gap: 0.25rem;
    cursor: pointer;
    user-select: none;
}

/*
 * A sub-row holds its place when the table scrolls sideways, exactly as the row above it does.
 *
 * The cells inherit .sticky-start / .sticky-end from their column, so the frozen first column stays
 * frozen through the sets. A cell needs an opaque background to be frozen over - without it the
 * columns it passes read straight through - and the sub-row's own tint is what it has to match, not
 * the white the parent row's frozen cells use.
 */
.vue-table .table-cell.parameter-sub-cell.sticky-start,
.vue-table .table-cell.parameter-sub-cell.sticky-end {
    background: var(--parameter-band-sub-bg, var(--bs-body-bg, #fff)) !important;
    background-color: var(--bs-body-bg, #fff);
}

/*
 * A caption spans every column, so freezing the element itself would do nothing - it is already as
 * wide as the grid. Its text is frozen instead, which keeps the conditions readable however far
 * right the reader has scrolled.
 */
.vue-table .parameter-sub-common .parameter-sub-sticky,
.vue-table .parameter-sub-group .parameter-sub-group__label,
.vue-table .parameter-sub-empty > * {
    position: sticky;
    left: 0.75rem;
}

/*
 * What is left of a folded band: a bar, in the header row and down every row below it.
 *
 * Folding used to keep the band's first column at whatever width it had, so "hide the physical
 * columns" left one wide empty column behind - the fold saved nothing and looked like a fault. The
 * column keeps its track because the band needs one to sit on and to be clicked open again, but the
 * track is 28px and nothing is drawn in it.
 */
.vue-table .table-header.is-band-collapsed,
.vue-table .table-cell.is-band-collapsed {
    padding-left: 0;
    padding-right: 0;
    overflow: hidden;
}

.vue-table .table-header.is-band-collapsed > *,
.vue-table .table-cell.is-band-collapsed > * {
    display: none;
}

/* A run of the material's own columns has no table to name, so it is a spacer and not a control. */
.vue-table .table-band.is-plain {
    cursor: default;
    /* Opaque, not transparent: it is sticky now, and rows scrolling visibly through a gap in the
       band row reads as a rendering fault. Quiet enough to stay a spacer. */
    background-color: var(--bs-body-bg, #fff);
    border-bottom-color: transparent;
}

/*
 * The spacer over the frozen columns is frozen with them. Otherwise, scrolled sideways, the next band's
 * label slid in above the frozen headings - "ATERIAL" over the Test column. Above the other bands, which
 * pass under it the way the columns pass under the frozen ones.
 */
.vue-table .table-band.is-pinned-start {
    left: 0;
    z-index: 5;
}

.vue-table .table-band.is-pinned-end {
    right: 0;
    z-index: 5;
}

/*
 * Only a header that follows a band is pushed down; without bands nothing matches and top: 0 holds.
 *
 * Three rules set `top` on a header - div.table-header, .table-header.sticky-start and
 * .sticky-end - so this has to outrank all of them: two classes plus a type is (0,3,1) against
 * their (0,1,1) and (0,2,0), which it does without !important.
 */
.vue-table .table-band ~ div.table-header {
    top: var(--parameter-band-height);
}

.vue-table .table-band:not(.is-plain):hover,
.vue-table .table-band:not(.is-plain):focus-visible {
    color: var(--bs-primary);
    background-color: rgba(var(--bs-primary-rgb), 0.08);
}

.vue-table .table-band__chevron {
    font-size: 0.6rem;
    transition: transform 0.12s ease;
}

.vue-table .table-band.is-collapsed .table-band__chevron {
    transform: rotate(-90deg);
}

/* Folded to one column, the label would push the column wider than the data ever needs it. */
.vue-table .table-band.is-collapsed .table-band__label {
    display: none;
}

/*
 * A folded band has to stay findable without hovering.
 *
 * Its label is gone and its column is a 28px track, so the only thing left was a 0.6rem chevron in
 * the grey band row - the group read as an empty gap and only showed itself on hover. A folded band
 * now shows a solid primary dot with a plus, softly glowing, that says "there is more here, open
 * it", and a faint glow line runs down the folded track through the header and every row, so the
 * fold stays visible wherever the table is scrolled. The line is a gradient over the existing
 * background, so the sticky header stays opaque. The band's title still names the table.
 */
.vue-table .table-band__expand {
    display: none;
}

.vue-table .table-band.is-collapsed {
    justify-content: center;
    padding-left: 0;
    padding-right: 0;
    /* the glow is drawn outside the dot and must not be clipped by the band cell */
    overflow: visible;
}

.vue-table .table-band.is-collapsed .table-band__chevron {
    display: none;
}

.vue-table .table-band.is-collapsed .table-band__expand {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    font-size: 13px;
    line-height: 1;
    color: #fff;
    background-color: rgba(var(--bs-primary-rgb), 0.85);
    box-shadow: 0 0 8px 2px rgba(var(--bs-primary-rgb), 0.45);
    transition: box-shadow 0.12s ease, background-color 0.12s ease;
}

.vue-table .table-band.is-collapsed:hover,
.vue-table .table-band.is-collapsed:focus-visible {
    background-color: var(--bs-gray-100);
}

.vue-table .table-band.is-collapsed:hover .table-band__expand,
.vue-table .table-band.is-collapsed:focus-visible .table-band__expand {
    background-color: var(--bs-primary);
    box-shadow: 0 0 11px 4px rgba(var(--bs-primary-rgb), 0.55);
}

.vue-table .table-header.is-band-collapsed,
.vue-table .table-cell.is-band-collapsed {
    background-image: linear-gradient(90deg, transparent 40%, rgba(var(--bs-primary-rgb), 0.18) 50%, transparent 60%);
}

@media (prefers-reduced-motion: reduce) {
    .vue-table .table-band__chevron,
    .vue-table .table-band__expand {
        transition: none;
    }
}

/* --- condition chips ----------------------------------------------------- */
.parameter-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.2rem;
    padding: 0.1rem 0.45rem;
    border-radius: 999px;
    background-color: var(--parameter-accent-soft);
    color: var(--parameter-accent);
    border: 1px solid var(--parameter-accent-border);
    font-size: 0.75rem;
    font-weight: 600;
    line-height: 1.4;
    white-space: nowrap; /* "23 °C" and "Precipitation hardened" must not break mid-value. */
}

/* Overflow counter on the collapsed parent row - a count, not a condition. */
.parameter-chip--more {
    background-color: var(--parameter-chip-muted-bg);
    color: var(--parameter-chip-muted-color);
    border-color: var(--parameter-chip-muted-border);
    font-weight: 500;
}

/* ── columns with display.maxLength ───────────────────────────────────────── */
/*
 * A cell shortened to its column's character budget. It is a <button> so it can
 * be tabbed to and pressed, so everything a button brings and this cell does not
 * want has to be taken back off it: the chrome, the padding, the fixed font, and
 * the left alignment a button does not have.
 *
 * `display: inline` rather than inline-block on purpose - the shortened value
 * wraps inside the cell exactly as the untruncated one does, so the two
 * spellings of a cell are not laid out differently.
 */
.cell-truncated {
    display: inline;
    padding: 0;
    border: 0;
    margin: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: inherit;
    cursor: pointer;
}

/*
 * The affordance is on the ellipsis, not on the text: underlining the value
 * itself would compete with the links and the search-term highlighting that the
 * value already carries, and in a table full of them it reads as noise.
 */
.cell-truncated__ellipsis {
    color: var(--bs-primary);
    font-weight: 600;
    padding-left: 0.1rem;
}

.cell-truncated:hover .cell-truncated__ellipsis,
.cell-truncated:focus-visible .cell-truncated__ellipsis {
    text-decoration: underline;
}

/* Keyboard focus has to be visible on something with no border of its own. */
.cell-truncated:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
    border-radius: 2px;
}

/*
 * The whole value in the dialog. Newlines in a remark are meaningful and the
 * server sends them as newlines, so they are honoured here even though the cell
 * could not afford to; break-word is what keeps an unbroken identifier from
 * widening the dialog past the viewport.
 */
.cell-full-value {
    white-space: pre-wrap;
    overflow-wrap: break-word;
}
.bulk-edit-modal {
    background: rgba(0, 0, 0, .5);
    z-index: 1060;
}

.bulk-edit-modal .modal-dialog {
    max-width: min(1140px, calc(100vw - 2rem));
}

.bulk-edit-modal .card-header small {
    font-weight: 400;
}

/*
 * Column filter modal (FilterModal in search-filters.js). Its filters render their option lists
 * inline (mode="list") instead of as floating dropdowns, so nothing covers the footer buttons.
 * The lists are bounded so the footer stays on screen without scrolling the page; the height
 * leaves room for the header, the control above the list and the footer.
 */
.filter-modal {
    --filter-list-max-height: max(6rem, min(18rem, calc(100vh - 22rem)));
    --smart-select-list-max-height: var(--filter-list-max-height);
}
@supports (height: 100dvh) {
    .filter-modal {
        --filter-list-max-height: max(6rem, min(18rem, calc(100dvh - 22rem)));
    }
}
.filter-modal .modal-header {
    gap: .5rem;
}
.filter-modal__footer {
    gap: .5rem;
}
.filter-modal__footer > * {
    margin: 0;
}
.filter-modal__footer > .me-auto {
    margin-right: auto;
}
@media (max-width: 575.98px) {
    .filter-modal .modal-dialog.filter-modal__dialog {
        margin: 0;
        width: 100%;
        max-width: none;
    }
    .filter-modal .modal-content {
        border-radius: 0;
    }
}

/* Contains / Starts with / Ends with / Exact above a string filter's input. */
.filter-match-modes {
    display: flex;
    width: 100%;
    margin-bottom: .5rem;
}
.filter-match-modes__option {
    flex: 1 1 0;
    min-width: 0;
    padding-left: .25rem;
    padding-right: .25rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    color: var(--sl-600, #495057);
    background: var(--bs-body-bg, #fff);
    border: 1px solid var(--sl-200, #ced4da);
}
.filter-match-modes__option:hover {
    color: var(--brand-800, #a84500);
    background: var(--brand-50, #fff6f0);
}
.filter-match-modes__option.is-active,
.filter-match-modes__option.is-active:hover {
    color: var(--brand-800, #a84500);
    background: var(--brand-50, #fff6f0);
    border-color: var(--brand-500, #ff7b1f);
    font-weight: 600;
    position: relative;
    z-index: 1;
}

/* StringSuggestions: the highlighted suggestion, in both modes. */
.filter-suggest__option.is-active {
    background: var(--brand-50, #fff6f0);
    color: var(--brand-800, #a84500);
}
.filter-suggest__list {
    list-style: none;
    margin: .5rem 0 0;
    padding: .25rem 0;
    max-height: var(--filter-list-max-height, 16rem);
    overflow-y: auto;
    border: 1px solid var(--sl-200, var(--bs-border-color, #ced4da));
    border-radius: var(--shell-radius, .375rem);
}
/* One line per suggestion, same height for every row, in both modes; the full value is the title. */
.filter-suggest__option {
    display: block;
    padding: .35rem .75rem;
    line-height: 1.4;
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.filter-suggest__state {
    margin-top: .5rem;
    padding: .5rem .75rem;
    color: var(--sl-500, var(--bs-secondary-color, #6c757d));
    font-size: .875em;
    border: 1px dashed var(--sl-200, var(--bs-border-color, #ced4da));
    border-radius: var(--shell-radius, .375rem);
}
