﻿/*
    Additions to the theme. Theme variables are used rather than overriding with !important. Keep
    this file small — if a rule here is fighting the theme, the markup is usually wrong.
*/

.page-title {
    margin-bottom: 0;
}

/* Audit detail panes: keep long JSON readable without breaking the table's width.
   The theme's base `pre` rule sets color: var(--tblr-light), meant to pair with its own dark
   code-block background. This one uses a light surface instead, so the text color must be
   overridden too or it renders unreadably light-on-light. */
.table pre {
    white-space: pre-wrap;
    word-break: break-word;
    font-size: 0.75rem;
    color: var(--tblr-body-color);
    background: var(--tblr-bg-surface-secondary);
    padding: 0.5rem;
    border-radius: var(--tblr-border-radius);
}

/* The change log's table.

   Fixed, so that nothing a record holds decides how wide the table is. A name can be four hundred
   characters without a space in it, and the detail under a row can hold a line of a file longer
   still. Under automatic layout the widest of them sets the width of the whole table, every column
   is spread to suit it, and the row's own controls are carried off the edge of the screen. Under
   this layout the columns are the widths named below, the record column takes what is left, and
   whatever is inside a column wraps.

   The floor keeps a narrow window scrolling inside the table's own container rather than squeezing
   the columns to nothing. */
.audit-table {
    table-layout: fixed;
    min-width: 58rem;
}

.audit-table .audit-col-when {
    width: 10rem;
}

.audit-table .audit-col-who {
    width: 16%;
}

.audit-table .audit-col-what {
    width: 28%;
}

.audit-table .audit-col-detail {
    width: 6rem;
}

.audit-table > tbody > tr > td,
.audit-table > thead > tr > th {
    overflow-wrap: anywhere;
}

/* A name, or the list of fields an entry changed, held to two lines with the whole of it on hover.
   A name is whatever somebody typed: one very long one is otherwise one very tall row. */
.audit-clamp {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    overflow-wrap: anywhere;
}

/* The action's label is the longest thing in its cell, so it wraps instead of pushing its
   neighbours, and reads from the left when it does. */
.audit-action {
    white-space: normal;
    justify-content: flex-start;
    text-align: start;
    -webkit-user-select: text;
    user-select: text;
}

/* The row's control names what it does while it is closed and what it will do while it is open. */
.js-audit-detail[aria-expanded="true"] .audit-label-open,
.js-audit-detail[aria-expanded="false"] .audit-label-close {
    display: none;
}

/* The panel that opens under a row, set apart from the list on a ground of its own for the reason
   the kiosk device panel is: without one it reads as more rows of the table above, and the row it
   belongs to scrolls out of sight. */
.audit-detail-cell {
    background: var(--tblr-bg-surface-secondary);
    box-shadow:
        inset 0 1px 0 var(--tblr-border-color),
        inset 0 -1px 0 var(--tblr-border-color);
}

.audit-detail {
    max-width: 100%;
    padding: .25rem .5rem .5rem;
    font-size: .875rem;
}

/* The comparison inside the panel. A table of its own on the panel's ground, so it does not paint
   the card's colour back over it, and with the card table's first-column indent taken off: the
   panel already has its own margin. */
.audit-detail .audit-changes {
    --tblr-table-bg: transparent;
    table-layout: fixed;
    width: 100%;
}

.audit-detail .audit-changes > :not(caption) > * > * {
    background: transparent;
}

.audit-table .audit-changes tr > :first-child {
    padding-left: .5rem;
}

.audit-table .audit-changes tr > :last-child {
    padding-right: .5rem;
}

.audit-detail .audit-changes th.audit-field-name {
    width: 11rem;
    font-weight: 500;
    color: var(--tblr-secondary);
    text-transform: none;
    letter-spacing: normal;
    font-size: inherit;
    white-space: normal;
    overflow-wrap: anywhere;
}

/* What a field held before, and what it holds now. A soft tint each, in the theme's own tints, so
   the eye finds the two sides without the text being drawn in a colour that is hard to read. */
.audit-detail .audit-changes .audit-was {
    background: var(--tblr-red-lt);
}

.audit-detail .audit-changes .audit-now {
    background: var(--tblr-green-lt);
}

.audit-detail .audit-changes td {
    overflow-wrap: anywhere;
}

/* What was added to a list and what was taken out of it. The items wrap and the run is bounded, so
   a long list being given to a new role is one panel and not a page. */
.audit-list-change {
    display: flex;
    align-items: baseline;
    gap: .5rem;
    margin-bottom: .25rem;
}

.audit-list-change > .badge {
    flex: 0 0 auto;
}

.audit-list-items {
    min-width: 0;
    max-height: 9rem;
    overflow: auto;
    overflow-wrap: anywhere;
}

/* One long value out of an entry: a file as it stood on a kiosk, or a passage too long for a table
   cell. Bounded and scrolled inside itself so neither the page nor the table is ever pushed
   sideways by it, and on the same ground as the rest of the panel's controls. */
.audit-detail .audit-block {
    margin: 0;
    max-height: 18rem;
    overflow: auto;
    padding: .5rem .75rem;
    font-size: .8125rem;
    line-height: 1.45;
    color: var(--tblr-body-color);
    background: var(--tblr-bg-surface);
    border: 1px solid var(--tblr-border-color);
    border-radius: var(--tblr-border-radius);
}

/* A file keeps its line breaks and its indentation, and scrolls in both directions: what is copied
   from it is pasted into something that reads it, so its layout is part of what it says. */
.audit-detail pre.audit-block {
    white-space: pre;
    word-break: normal;
    tab-size: 4;
}

/* A long passage with no line breaks is wrapped: a horizontal scrollbar on a paragraph cannot be
   read. It is still one line when it is copied. */
.audit-detail .audit-block-wrap {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* Wide tables scroll inside their own container; the page itself never scrolls sideways. */
.table-responsive {
    overflow-x: auto;
}

/* The row under the pointer is tinted, so the eye can follow a wide row across to its controls at
   the end. Every table gets it from here rather than each page opting in, in the theme's own hover
   colour so it follows the theme and the brand.

   A row whose only cell spans the whole table is a message rather than a record — nothing matched,
   still loading — and is left untinted: lighting it would suggest there is something to select. */
.table > tbody > tr:not(:has(> td:only-child[colspan])):hover > * {
    --tblr-table-color-state: var(--tblr-table-hover-color);
    --tblr-table-bg-state: var(--tblr-table-hover-bg);
}

/*
    Branding.

    The marks are in wwwroot/brand. The colours, the sign-in photograph and how far it is dimmed
    are the deployment's own and arrive in a second stylesheet served after this one, so nothing
    here names a colour or an image: this file lays the pieces out, and that one paints them.
*/

/* Sign-in and the other unauthenticated pages sit over a photograph, dimmed by a scrim so that
   card text stays legible whatever part of the image lands behind it. The image and the scrim's
   strength are painted by the deployment's stylesheet. */
.login-page {
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    background-attachment: fixed;
}

.login-page::before {
    content: "";
    position: fixed;
    inset: 0;
    pointer-events: none;
}

/* Everything the page draws has to sit above that scrim. */
.login-page > * {
    position: relative;
}

/* The client's mark, at the head of the card on each signed-out page. It sits on the card rather
   than on the photograph, so this is the navy rendition. */
.login-brand-block {
    margin-bottom: 1.5rem;
}

/* Height from the brand profile, because how big a mark needs to be is a property of the artwork:
   a horizontal strip and a nearly square lock-up standing at one height come out at very different
   widths. The width stays auto, so the mark keeps the proportions it was drawn in. The fallback is
   the figure this rule carried before the setting existed, so a profile that says nothing is
   unchanged. */
.login-brand {
    height: var(--brand-logo-height, 2.75rem);
    width: auto;
}

/* The same mark at the head of the side menu, which the theme sizes at 2rem for every brand. Its
   own figure rather than the card's: this row also holds the collapse control and sets the offset
   of every entry below it, so the two places want different answers. Written bare, as the theme's
   own rule is, and this stylesheet loads after it. */
.navbar-brand-image {
    height: var(--brand-sidebar-logo-height, 2rem);
    width: auto;
}

/* An h1, so the sign-in card is not a page without a heading now that its own title has gone.
   Sized as the line it is rather than as a first-level heading, and its own margins stated so it
   does not inherit the theme's heading spacing. */
.login-brand-title {
    margin-top: 0.75rem;
    margin-bottom: 0;
    font-size: 1.125rem;
    font-weight: var(--tblr-font-weight-medium);
    line-height: 1.2;
    color: var(--tblr-body-color);
}

/* The notice on the sign-in card that says a session was ended rather than simply having been
   left. It is the first thing a returning visitor needs to read, and left to the theme's tinted
   box it is a near-white panel on a white card — present, and the faintest thing in front of
   them. This raises the tint, states the border at a strength that can be seen, and stands an
   accent down the leading edge, while leaving the words themselves at full body colour: dark
   text on a light wash reads at a far higher contrast than white on a filled block would, and it
   does not turn the notice into a third button above the two real ones. */
.login-notice {
    /* Stated outright rather than through the tint variable, which the box then mixes half-and-half
       with the surface again and washes back out to something very near the card's own white. */
    background-color: color-mix(in srgb, var(--tblr-alert-color) 20%, var(--tblr-bg-surface));
    align-items: flex-start;
    gap: 0.75rem;
    border-color: color-mix(in srgb, var(--tblr-alert-color) 55%, transparent);
    border-inline-start: 0.25rem solid var(--tblr-alert-color);
    color: var(--tblr-body-color);
}

/* Darkened, because the accent colour that reads well as a border is thin at glyph weight over
   its own wash. */
.login-notice .alert-icon {
    color: color-mix(in srgb, var(--tblr-alert-color) 78%, #000000);
}

/* The fact, above the sentence that says what to do about it. Sized as the line it is rather
   than as a second-level heading, and its own margins stated so it does not inherit the theme's
   heading spacing. */
.login-notice .alert-title {
    margin-top: 0;
    margin-bottom: 0.125rem;
    font-size: 1rem;
    font-weight: var(--tblr-font-weight-bold);
    line-height: 1.3;
    color: var(--tblr-body-color);
}

/* Panashi's own mark on the sign-in pages, pinned to the bottom right of the window rather than
   placed under the card: the card is centred and its height varies from page to page, so a mark
   below it would sit at a different place on each one. Fixed, so a page that scrolls keeps it
   where it is. */
.login-vendor-logo {
    position: fixed;
    right: 1.5rem;
    bottom: 1.25rem;
    line-height: 0;
}

.login-footer-logo {
    height: 3.25rem;
    width: auto;
}

@media (max-width: 575.98px) {
    .login-vendor-logo {
        right: 1rem;
        bottom: 0.75rem;
    }

    .login-footer-logo {
        height: 2.25rem;
    }
}

/* The footer mark on authenticated pages, which have a light background. */
.footer-logo {
    height: 2.75rem;
    width: auto;
}

/* The product name, in the top bar opposite the user menu. The navbar container is already a
   space-between flex row, so the heading holds the left edge and the user menu the right. */
.navbar-heading {
    font-size: 1.125rem;
    font-weight: var(--tblr-font-weight-medium);
    line-height: 1.2;
    color: var(--tblr-body-color);
}

@media (max-width: 575.98px) {
    .navbar-heading {
        font-size: 1rem;
    }
}

/* An uploaded user photo, cropped to fill its box rather than stretched. It is an <img src>
   rather than the theme's avatar pattern, which needs a style= attribute the policy blocks. */
.img-cover {
    object-fit: cover;
}

/* ---------------------------------------------------------------------------------------------
   The sidebar.

   An entry is a flex row — the icon, then the name — and the theme leaves the icon free to shrink
   while the name cannot: a name is one unbroken run of text. So the whole of the overflow a long
   name causes is taken out of the icon beside it, and that one entry draws a narrower icon and
   starts its name a few pixels to the left of every entry around it, which is exactly the case a
   column of entries makes obvious. Holding the icon at its size clips nothing: the longest name
   in the sidebar is wider than the room its own padding leaves it, not wider than the sidebar.

   A sub-entry's icon has a second problem of its own. Its box is taller than the line its glyph
   is set on, and the glyph is drawn at the top of that box rather than in the middle of it, so it
   rides above the name it labels. Centring the glyph in its box puts the two back on one line.
   An entry in the level above already centres its own icon, which is why only the sub-entries
   look wrong.
   --------------------------------------------------------------------------------------------- */
.navbar-vertical .nav-link-icon,
.navbar-vertical .dropdown-item-icon {
    flex: 0 0 auto;
}

.navbar-vertical .dropdown-item-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

/* The current sub-entry.

   The theme marks it with two tenths of black and nothing else, which over a navy panel is a
   shade rather than a mark: at a glance the column reads as though nothing in it is selected.
   The entry one level up is marked with a bar down its left edge and full-strength text, so the
   sub-entry is given the same two. It is the same idea one level in, and a reader who has learned
   one has learned the other.

   Three signals rather than one, deliberately: colour alone is not something every reader can
   use, so the bar carries the meaning on its own and the weight carries it again. The colours are
   the sidebar's own active variables, so a theme change moves this with it.

   The fill is a tint of the flyout's own text colour rather than of black, which is the part
   that does the work: two tenths of black over a dark panel is 1.10:1 against it — a shade at
   the edge of what an eye resolves, and one that reads as a shadow rather than a selection. A
   sixth of the text colour is 1.67:1 on the panel as shipped and reads as raised, and it stays a
   tint of the text on a light panel too, where black would have been right and white invisible.
   The bar is the same variable the entry one level up already draws its bar from. Both values
   are painted by the deployment's stylesheet. */
.navbar-vertical .dropdown-menu .dropdown-item.active {
    position: relative;
    background: var(--tblr-dropdown-link-active-bg);
    color: var(--tblr-navbar-active-color);
    font-weight: 600;
}

.navbar-vertical .dropdown-menu .dropdown-item.active::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    border-left: 3px solid var(--tblr-navbar-active-border-color);
}

/* ---------------------------------------------------------------------------------------------
   Command centre.

   These exist as classes rather than style attributes because the Content Security Policy blocks
   an inline style attribute in exactly the same way it blocks an inline script.
   --------------------------------------------------------------------------------------------- */

/* The two scrolling panes on Api Logs. A kiosk can return two thousand entries; without a bound
   the list runs off the page and the detail pane scrolls with it instead of beside it. */
.api-log-pane {
    max-height: 32rem;
}

/* One request or response body inside the detail pane. Wrapped rather than clipped: a body is
   frequently one long line of JSON, and a horizontal scrollbar per section is unreadable. */
.api-log-body {
    white-space: pre-wrap;
    max-height: 12rem;
    overflow-y: auto;
}

/* The kiosk's answer in the command history list. Truncated to keep one row one line — the whole
   message is in the detail panel. */
.command-answer {
    max-width: 24rem;
}

/* ---------------------------------------------------------------------------------------------
   Read-only fields.

   The theme greys a disabled control and leaves a read-only one looking exactly like an editable
   one, so a value that is shown but cannot be changed — an account's sign-in address, a form's
   only merchant — read as an invitation to type. A read-only field takes the disabled look. The
   two differ in one thing only, which is why both exist: a read-only value is still posted.
   --------------------------------------------------------------------------------------------- */

.form-control[readonly] {
    background-color: var(--tblr-bg-surface-secondary);
    color: var(--tblr-secondary);
}

/* ---------------------------------------------------------------------------------------------
   Date entry.

   The panel is built from the theme's own `.calendar` component and `.dropdown-menu` surface —
   see wwwroot/js/date-picker.js for why the browser's own date popup is not used. What is here is
   only the joinery the theme does not ship: putting the calendar inside a dropdown, the
   selected-day state, and the month grid.
   --------------------------------------------------------------------------------------------- */

/* The positioning context for the panel. The field is `.input-icon`, nested inside, so the
   calendar mark sits in the field and the panel hangs off the whole thing. */
.date-picker {
    position: relative;
}

/* `.dropdown-menu` already brings the surface, border, radius and shadow, and `.show` displays
   it; the panel is positioned by these rules rather than by a dropdown component. Width is fixed
   because the calendar's seven columns are percentages. */
.date-picker-panel {
    top: 100%;
    left: 0;
    width: 18.5rem;
    margin-top: 0.25rem;
    padding: 0;
}

/* Flipped only when the panel would otherwise leave the viewport — a filter row near the bottom of
   a long page, or a field at the right-hand edge. */
.date-picker-panel-above {
    top: auto;
    bottom: 100%;
    margin-top: 0;
    margin-bottom: 0.25rem;
}

.date-picker-panel-end {
    right: 0;
    left: auto;
}

/* The dropdown already draws a border; the calendar's own would be a second one just inside it. */
.date-picker-panel .calendar {
    padding: 0 0.5rem;
    border: 0;
}

.date-picker-panel .calendar-nav {
    padding: 0.25rem 0;
}

.date-picker-nav-btn {
    width: 1.75rem;
    padding: 0;
}

/* `.calendar-title` supplies flex: 1 and the centring; this is the button reset over the top. */
.date-picker-title {
    padding: 0.125rem 0.5rem;
    font-size: 0.8125rem;
    font-weight: var(--tblr-font-weight-bold);
}

/* The theme's `.date-item` is an <a>. These are buttons, so the panel is operable from the
   keyboard and a disabled day is genuinely unclickable. Raised to 1.75rem, since the theme's
   1.4rem is a 22px hit target and 24px is the minimum a pointer target should meet. */
.date-picker-panel .date-item {
    width: 1.75rem;
    height: 1.75rem;
    padding: 0;
    font: inherit;
    line-height: 1.75rem;
}

.date-picker-panel .date-item.date-selected {
    color: #fff;
    background: var(--tblr-primary);
    border-color: var(--tblr-primary);
}

/* Out of range — before the "from" date on a "to" field, or outside a min/max the page set. Shown
   rather than hidden, so the month keeps its shape and it is obvious why the day cannot be taken. */
.date-picker-panel .date-item:disabled {
    color: var(--tblr-disabled-color);
    cursor: not-allowed;
    background: none;
    border-color: transparent;
    opacity: 0.4;
}

/* The month grid, reached by selecting the month name. Stepping a year at a time through the
   chevrons is how a validity date years away is reached without forty clicks. */
.date-picker-months {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.25rem;
    padding: 0.5rem 0;
}

.date-picker-month {
    padding: 0.375rem 0.25rem;
    color: var(--tblr-body-color);
    background: none;
    border: var(--tblr-border-width) var(--tblr-border-style) transparent;
    border-radius: var(--tblr-border-radius);
    transition: background 0.15s ease-in-out, color 0.15s ease-in-out;
}

.date-picker-month:hover {
    color: var(--tblr-primary);
    border-color: var(--tblr-border-color);
}

.date-picker-month.date-selected {
    color: #fff;
    background: var(--tblr-primary);
    border-color: var(--tblr-primary);
}

/* Hour and minute, between the calendar and the panel's actions. Only a field that carries a time
   has this row — see wwwroot/js/date-picker.js. */
.date-picker-time {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.5rem;
    border-top: var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);
}

/* Pushed left, with the two lists kept together against the right-hand edge. */
.date-picker-time-label {
    margin-right: auto;
    font-size: 0.8125rem;
    color: var(--tblr-secondary);
}

/* Two digits and the pair of chevrons beside them. The chevrons stretch to whatever height the
   field is, so the two halves stay square with each other if the field metrics ever change. */
.date-picker-time-field {
    display: flex;
    align-items: stretch;
}

.date-picker-time-input {
    width: 2.5rem;
    padding-right: 0.25rem;
    padding-left: 0.25rem;
    text-align: center;
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
}

.date-picker-time-steppers {
    display: flex;
    flex-direction: column;
}

/* Split evenly down the field's height rather than given one of their own, for the same reason the
   field's width is the only figure stated here: one of them changing must move the other. */
.date-picker-time-step {
    display: flex;
    flex: 1 1 0;
    align-items: center;
    justify-content: center;
    width: 1.125rem;
    padding: 0;
    font-size: 0.625rem;
    line-height: 1;
    color: var(--tblr-secondary);
    background: none;
    border: var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);
    transition: color 0.15s ease-in-out, background 0.15s ease-in-out;
}

.date-picker-time-step:first-child {
    border-bottom: 0;
    border-top-right-radius: var(--tblr-border-radius);
}

.date-picker-time-step:last-child {
    border-bottom-right-radius: var(--tblr-border-radius);
}

.date-picker-time-step:hover {
    color: var(--tblr-primary);
    background: var(--tblr-bg-surface-secondary);
}

.date-picker-time-separator {
    color: var(--tblr-secondary);
}

/* Clear and Today, on the panel's own footer rule. */
.date-picker-actions {
    display: flex;
    align-items: center;
    padding: 0.25rem 0.5rem;
    border-top: var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);
}

/* ---------------------------------------------------------------------------------------------
   Searchable dropdowns.

   The dropdown's own skin reads its colours and radii from `--bs-*` custom properties, which the
   theme does not define — it uses `--tblr-*` for the same set. Without this bridge every
   `var(--bs-*)` below resolves to nothing, dropping the control's background, border and radius
   rather than merely mis-colouring them.
   --------------------------------------------------------------------------------------------- */
.ts-wrapper, .ts-dropdown {
    --bs-body-bg: var(--tblr-body-bg);
    --bs-body-color: var(--tblr-body-color);
    --bs-border-color: var(--tblr-border-color);
    --bs-border-color-translucent: var(--tblr-border-color-translucent);
    --bs-border-radius: var(--tblr-border-radius);
    --bs-border-radius-lg: var(--tblr-border-radius-lg);
    --bs-border-radius-sm: var(--tblr-border-radius-sm);
    --bs-border-width: var(--tblr-border-width);
    --bs-box-shadow-inset: var(--tblr-box-shadow-inset);
    --bs-form-invalid-color: var(--tblr-form-invalid-color);
    --bs-form-valid-color: var(--tblr-form-valid-color);
    --bs-secondary-bg: var(--tblr-secondary-bg);
    --bs-secondary-color: var(--tblr-secondary-color);
    --bs-tertiary-bg: var(--tblr-tertiary-bg);
}

/* A searchable dropdown stands exactly as tall as the fields and buttons beside it.

   The skin puts the theme's own `form-select` class on the wrapper it builds, and then throws away
   the two things that class was carrying: `padding: 0 !important` on the wrapper, and its own
   `padding` and `line-height` on the control inside. Those replacements are the skin's own stock
   figures rather than the theme's, and the theme's field is the taller of the two -- so every
   dropdown in the portal came out shorter than the input, the date field and the button standing
   next to it. In a filter row, which is bottom-aligned, the whole column drops by the difference
   and takes its label with it: the visible symptom was a row of labels that did not sit on one
   line.

   Restated here rather than derived, because the theme bakes these into `.form-control` instead of
   exposing them as custom properties. A build check reads both stylesheets and fails if the two
   stop agreeing, so a theme upgrade cannot quietly reintroduce the gap. */
.ts-wrapper.form-select > .ts-control,
.ts-wrapper.form-control > .ts-control {
    padding-top: 0.5625rem;
    padding-bottom: 0.5625rem;
    padding-left: 1rem;
    line-height: 1.25rem;
}

/* The same override drops the theme's inset field shadow, which is what stopped a searchable
   dropdown reading as the same kind of control as the input beside it. The right-hand padding is
   deliberately left to the skin: it computes it from whichever of the caret and the clear button
   the control is actually showing. */
.ts-wrapper.form-select,
.ts-wrapper.form-control {
    box-shadow: var(--tblr-shadow-input);

    /* The skin also floors the wrapper at a field height of its own. The padding above already
       clears that floor, so today it changes nothing -- but it is a second figure that would
       silently take over and reintroduce the gap the moment the theme's field became the shorter
       of the two. The control's own box is the one thing deciding this height. */
    min-height: 0;
}

/* The skin draws no focus ring of its own, so the theme's field focus state is drawn here and a
   searchable dropdown does not stand out as the one field that behaves differently. */
.ts-wrapper.focus > .ts-control {
    border-color: var(--tblr-primary);
    box-shadow: 0 0 0 0.25rem rgba(var(--tblr-primary-rgb), 0.25);
}

/* The dropdown panel sits above other page chrome; the theme's stacking context starts higher
   than the skin's default z-index of 1. */
.ts-dropdown {
    z-index: 1030;
}

/* A chosen value never decides how wide the control is.

   The native control this replaces is as wide as its widest option, so picking a different one
   moves nothing. The replacement is as wide as the item currently in it, which in a filter row
   whose columns size to their contents means the box grows and shrinks as values are chosen -- and
   every control to its right slides along with it. A row that will not hold still is the reported
   symptom; the cause is here rather than on the pages it shows up on.

   The pair below is what makes a definite column width hold. Without the first, a long value still
   pushes the box past the column it was given -- a minimum content width is a floor the column has
   to respect, and zero is what lets the box be as narrow as it is told. The second is what a value
   too long for the space then does: one line, clipped, with an ellipsis, rather than wrapping and
   making that one control taller than the rest of the row. */
.ts-wrapper.form-select > .ts-control,
.ts-wrapper.form-control > .ts-control {
    min-width: 0;
}

.ts-wrapper.form-select > .ts-control > .item,
.ts-wrapper.form-control > .ts-control > .item {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* The chosen value steps aside while the person types.

   The skin keeps the current item on show beside its search box, so search text appears after
   the value it is about to replace -- after "Choose a command...", or after the value already
   chosen. The class is set by the shared dropdown script for as long as there is search text and
   cleared when the list closes, so the value reappears on its own when nothing new is picked. */
.ts-wrapper.single.searching > .ts-control > .item {
    display: none;
}

/* The search box shares the line with the chosen value instead of being pushed below it.

   The skin lays the control out as a wrapping row and gives the search box a minimum width of
   seven characters' worth, which is wider than the space left beside a value in any of the
   filter columns below. So the box wraps onto a second line the moment the control takes focus:
   the control doubles in height, the value sits on the first line and a caret blinks on the
   second, under a heading that now has a two-line field under it. It is worst where it is most
   often seen -- a short closed set such as an outcome sits in the narrowest column there is.

   Neither figure suits a single-value control, which has exactly one value and one search box to
   fit: the row does not wrap, and the box takes whatever is left beside the value rather than
   demanding a width of its own. A value too long for what remains is clipped by the rule above,
   and typing hides the value outright, so the box is never short of room for what is being
   typed. */
.ts-wrapper.single > .ts-control {
    flex-wrap: nowrap;
}

.ts-wrapper.single > .ts-control > input {
    min-width: 0;
}

/* The search box takes no room at all until there is something to type in it.

   A text input asks for about twenty characters' worth of space whether or not it holds anything,
   and the box beside it is only as wide as the longest value it was given. Allowed to shrink but
   still asking, the empty box and the chosen value shared out the shortfall between them -- so
   opening a dropdown cut the end off the value that was already in it, and a reader saw a value
   that looked wrong for as long as they were choosing. Closed it had been correct, which is what
   made it look like the list was doing something to the value.

   Asking for nothing is what fixes it: with no width of its own and nothing to grow into, the box
   leaves the whole control to the value. From the first keystroke the value steps aside -- the
   rule above -- this rule stops applying, and the box has the control to itself for whatever is
   being typed. */
.ts-wrapper.single:not(.searching) > .ts-control > input {
    flex: 0 0 0;
    width: 0;
}

/* The timezone filter, which stands beside the date fields on every page carrying a range.

   Its own options are the clearest case of the above: they are a zone name and an offset, and the
   three it offers are nothing like the same length. Given a width here rather than on each page,
   because it is one control rendered from one place and a width repeated on five pages is a width
   four of them will eventually disagree about.

   As wide as the longest label it can compose and no wider. Every label is a three-letter
   abbreviation and an offset, or an offset alone written against UTC, so none runs past nine
   characters. A longer one would be clipped by the rule above rather than widening the row. */
.filter-col-zone {
    flex: 0 0 auto;
    width: 9.5rem;
    max-width: 100%;
}

/* The rest of the filter controls: three sizes for a dropdown, and one for a date.

   Named widths rather than a figure per page: the pages that carry these rows were written at
   different times and a width written out on each of them is a width they will drift apart on.
   Pick a dropdown's by what the list holds, not by how wide the page happens to look -- a control
   sized to the longest value anybody has entered so far is a control that changes size when
   somebody enters a longer one.

     narrow  a short closed set: an outcome, a state, a product type
     medium  a name from a set an operator maintains: a branch, a category, a directory
     wide    a name from a set that grows on its own: a kiosk, a package, a schedule
     date    a single day, which is always written the same ten characters long

   A closed set whose labels are phrases rather than a word or two -- a lockdown state, what is on
   a kiosk's screen -- takes the narrowest of the three that shows every one of its labels whole,
   the one that means "any" included. Unlike a name nobody has typed yet, its longest label is
   known when the page is written, so there is never a reason to clip one. Leave room over the
   longest label rather than matching it: the same words are wider in one computer's font than in
   another's.

   Never a share of the page. A column that is a quarter of the row is a quarter of whatever
   screen it is on, which on a wide screen is far more than the value in it will ever need, and
   every pixel of that is taken from the controls beside it. A row of five such columns leaves no
   room at its end for its own Filter button, which then sits alone on a second line under an
   otherwise empty stretch of card. Sized by what they hold, the controls stay together on one
   line wherever the screen has room for them, and a row only wraps where it genuinely does not.

   From the small breakpoint up only. Below it these rows stack one control to a line, where the
   column class on the element already says full width and a fixed figure here would fight it. */
@media (min-width: 576px) {
    .filter-col-narrow,
    .filter-col-medium,
    .filter-col-wide,
    .filter-col-date,
    .filter-col-datetime {
        flex: 0 0 auto;
        max-width: 100%;
    }

    .filter-col-narrow {
        width: 9.5rem;
    }

    /* The date field's text, its padding and the calendar mark inside it, with room to spare. */
    .filter-col-date {
        width: 9.5rem;
    }

    /* The same with an hour and a minute after the date. */
    .filter-col-datetime {
        width: 13rem;
    }

    .filter-col-medium {
        width: 13rem;
    }

    .filter-col-wide {
        width: 18rem;
    }
}

/* The count that shares a list's card header with its filters: "28 kiosks, page 1 of 3".

   It stands after the controls and takes whatever room they leave. That order is the point: the
   controls then begin at the start of the header, so nothing that appears after them -- the clear
   button a search brings with it -- can move them. Placed first, the count would push them
   against the far end, where any change in their combined width moves every one of them.

   It keeps at least the width below. With less than that left beside the controls, it takes a
   line of its own under them rather than standing beside them as a column a word or two wide.
   Beside them it sits at the far end of the header; on a phone, where everything stacks, it reads
   from the left like the controls above it. */
.filter-col-count {
    flex: 1 1 15rem;
}

@media (min-width: 576px) {
    .filter-col-count {
        text-align: end;
    }
}

/* ---------------------------------------------------------------------------------------------
   Scroll-to-top.

   Hidden by default; the shared script adds `.show` once the page has scrolled past its
   threshold, so a short page never displays a control with nothing to do. z-index sits below the
   theme's modals (1050+) but above ordinary page content. */
.scroll-top-btn {
    position: fixed;
    right: 1.5rem;
    bottom: 1.5rem;
    z-index: 1030;
    opacity: 0;
    visibility: hidden;
    transform: translateY(0.5rem);
    transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out, visibility 0.2s ease-in-out;
}

.scroll-top-btn.show {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

/* ---------------------------------------------------------------------------------------------
   The kiosk log pane.

   The log is what its page is for, so the pane is given every pixel the page can spare. A log is
   read at its end, so the pane is a window onto the file that the newest lines are scrolled to,
   rather than a block that grows the page to half a million characters. Horizontal scrolling is
   the pane's, not the document's: log lines are long and a fault report must not push the whole
   page sideways.

   The height comes from the layout rather than from a figure. On this page alone the page frame
   is bound to the window, and every box between it and the pane - the wrapper, the body, its
   container, the card and the card body - is laid out as a column that fills the one above it.
   The pane takes whatever is left under the card header, so the whole of it, both scrollbars
   included, is on screen without the page having to scroll first. The footer is left off this
   page for the same reason: it would take a row from the log to show a logo that is on every
   other page.

   Nothing here is measured or set by a script, deliberately. The page reads the same whether or
   not its script ran, which a measured height cannot promise.

   `min-height: 0` at every level is the part that is easy to leave out and does not work
   without: an item in a column will not shrink below the height of its own contents unless it is
   told that it may, so without it the chain grows to the length of the file and bounds nothing.

   `color` and the scrollbar colour are set explicitly and neither is optional. The skin styles
   `pre` for a dark code block: its text is near-white, and its scrollbar thumb is a faint tint of
   that same near-white - invisible on the light surface this pane paints, which reads as the pane
   having no scrollbar at all. The thumb takes the body text colour instead, at the same faint
   tint the rest of the page's bars use. */
.page:has(.log-pane) {
    height: 100vh;
    height: 100dvh;
}

.page:has(.log-pane) > .page-wrapper,
.page:has(.log-pane) .page-body,
.page:has(.log-pane) .page-body > .container-fluid,
.log-host:has(.log-pane),
.card:has(> .card-body > .log-pane),
.card:has(> .card-body > .log-pane) > .card-body {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

.card:has(> .card-body > .log-pane) {
    margin-bottom: 0;
}

.page:has(.log-pane) > .page-wrapper > .footer {
    display: none;
}

/* The card header is one row: the kiosk and its file's facts on the left, the filter and the
   collect-again control on the right. The facts are given a basis to lay out from rather than
   their full unwrapped length, so the controls stay on the title row and the facts wrap under it
   when the window is narrow; only a window too narrow for both drops the controls to a row of
   their own. */
.log-summary {
    flex: 1 1 16rem;
    min-width: 0;
}

/* The filter sits in the card header beside the collect-again control, so it has a width of its
   own rather than the whole row's. */
.log-filter {
    width: 18rem;
}

.log-pane {
    --tblr-scrollbar-color: var(--tblr-body-color);

    flex: 1 1 auto;
    min-height: 14rem;
    overflow: auto;

    /* The pane is a window onto the file rather than a part of the page that flows, so reaching
       its last line does not then carry on and scroll the page underneath it. */
    overscroll-behavior: contain;
    margin: 0;
    padding: 1rem;
    color: var(--tblr-body-color);
    background: var(--tblr-bg-surface-secondary);

    /* Flush with the card, which draws the frame; only the card's own bottom corners are kept. */
    border: 0;
    border-radius: 0 0 var(--tblr-card-inner-border-radius) var(--tblr-card-inner-border-radius);
    font-size: 0.8125rem;
    line-height: 1.45;
    white-space: pre;
    tab-size: 4;
}

/* Where the relational selector above is not understood, the pane bounds itself and the page is
   what it was before: readable, with the pane's own bars reached by scrolling the page down to
   them. */
@supports not selector(:has(*)) {
    .log-pane {
        max-height: 60vh;
    }
}

/* ---------------------------------------------------------------------------------------------
   Update Centre.

   Three small things the theme has no class for. All of them exist because a table cell that
   truncates has to say so: a hover title alone is invisible until somebody happens to try it, and
   does not exist at all on a touch screen. */

/* "There is more here — select it." The same visual language as a native <abbr title>, but backed
   by a click, so it works on a touch screen and can be reached from the keyboard. The element is a
   <button> for that reason rather than a <span> with a handler. */
.js-truncated {
    max-width: 22rem;
    text-decoration: underline dotted;
    text-underline-offset: 3px;
}

/* The kiosk picker on the scheduling form and on the add-alert form. Bounded so the page's own
   actions stay on screen however many kiosks are listed; the list scrolls inside it, and each branch heading sticks to
   the top of the box while its kiosks scroll under it, so a long estate is always read by site. */
.update-terminal-picker {
    max-height: 26rem;
    overflow-y: auto;
}

.update-terminal-group-header {
    position: sticky;
    top: 0;
    z-index: 1;
    padding: 0.375rem 0.75rem;
    background-color: var(--tblr-bg-surface-secondary);
    border-bottom: var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);
}

.update-terminal-group + .update-terminal-group .update-terminal-group-header {
    border-top: var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);
}

.update-terminal-group-rows {
    padding: 0.375rem 0.75rem;
}

.update-terminal-group-rows .form-check {
    margin-bottom: 0.25rem;
}

/* The branch tick list, on the scheduling form and on the administration console. Bounded and
   scrolled inside itself, so an estate with many branches does not open a menu taller than the
   screen. One rule for both, so the two pickers cannot come to look different. */
.branch-filter-menu {
    max-height: 18rem;
    overflow-y: auto;
    min-width: 16rem;
}

/* The branch heading inside a kiosk table: a row of its own rather than a repeated column. Tinted
   and weighted so a body of rows reads as belonging to the name above it while scrolling. */
.kiosk-branch-row > td {
    background-color: var(--tblr-bg-surface-secondary);
}

/* The kiosks stand one step in from the branch heading above them, so the nesting is visible
   without having to read the tint. The heading itself is one cell spanning the row and is not
   touched by this, which is what makes the step readable: the branch names run down the table's own
   left edge and the kiosks under them do not.

   A transparent left border rather than a larger padding. The theme sets the first cell's padding
   itself, so a padding figure written here would have to restate that figure and then drift from it
   — the same trap a restated field metric falls into. A border sits outside the padding and adds
   to whatever it is,
   whatever it becomes. The theme clears this border on a card table's first cell, which is why the
   rule names the cell as specifically as it does. */
.card-table tr.kiosk-branch-child > td:first-child {
    text-indent: 1.5rem;
}

/* The fold button's chevron turns to point at what it will do next. */
.js-branch-fold[aria-expanded="false"] .ti {
    transform: rotate(180deg);
}

/* The last answer a kiosk gave, at the end of its row on the administration console. Given a floor
   so the answer stays readable beside a long kiosk name rather than being squeezed to a word a
   line; below that the table scrolls sideways inside its card, as it already does. An answer with
   no spaces in it, such as a folder path, breaks inside the cell rather than widening the table. */
.kiosk-last-result {
    min-width: 18rem;
    overflow-wrap: anywhere;
}

/* A package's changelog and its file list, in the details modal. Both are bounded and scroll
   inside themselves: a full-application package runs to a few thousand file rows, and a modal that
   grows to fit them is one nobody can close. */
.update-changelog {
    max-height: 20rem;
    overflow-y: auto;
}

.update-file-list {
    max-height: 20rem;
    overflow-y: auto;
}

/* ---------------------------------------------------------------------------------------------
   Notifications.

   Terminal Alerts lists two free-text messages per row. Unbounded they push the row's own actions
   off the side of the table, so the operator scrolls sideways to reach Deactivate and loses sight
   of which alert the row is. Bounded and truncated, with the full text on the cell's title.

   This lives here rather than as a style attribute on the cell, which the page's policy blocks
   outright — the attribute this replaced was being dropped by the browser, which is what made the
   table overflow in the first place. */
.terminal-alert-message {
    max-width: 16rem;
}

/* The whole of one alert, in the dialog the View control opens. A message is one line on the
   kiosk, but a long one has to wrap here rather than run off the edge. */
.terminal-alert-full {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------------------------------------
   Panashi Support.

   Four small things the theme has no class for. Each is here for the same reason the Update
   Centre's are: a pane that grows to fit its contents is a modal nobody can close, and a document
   that came off a kiosk has no bound on its size. */

/* One request or response document in the transaction detail panel. Wrapped rather than clipped —
   a payload is usually one long line of JSON, and a horizontal scrollbar per section is
   unreadable. `color` and the scrollbar colour are set for the same reason .log-pane sets them:
   the skin styles `pre` for a dark code block, so on this light background its text and its
   scrollbar thumb are both near-invisible, and a document longer than the box reads as one that
   simply stops. Reaching the end of it does not carry on and scroll the dialog underneath. */
.transaction-document {
    --tblr-scrollbar-color: var(--tblr-body-color);

    max-height: 18rem;
    overflow: auto;
    overscroll-behavior: contain;
    margin: 0;
    padding: 0.75rem;
    color: var(--tblr-body-color);
    background: var(--tblr-bg-surface-secondary);
    border: 1px solid var(--tblr-border-color);
    border-radius: var(--tblr-border-radius);
    font-size: 0.8125rem;
    line-height: 1.45;
    white-space: pre-wrap;
    tab-size: 4;
}

/* The kiosk picker on Manage Kiosk Configurations. Bounded so the page's own actions stay on
   screen with an estate of tens of kiosks listed; the list scrolls inside it. */
.kiosk-config-picker {
    max-height: 24rem;
    overflow-y: auto;
}

/* The configuration file editor.
   ---------------------------------------------------------------------------------------------
   Three layers in one box: a line-number gutter down the left, a coloured but non-interactive copy
   of the text, and the real textarea laid exactly over that copy with its own text made
   transparent so only its caret and selection show. The operator is typing into an ordinary
   textarea throughout, which is what keeps undo, find-in-page, spellcheck and assistive technology
   working — the colour is a picture behind the glass, not a replacement for the field.

   ONE element scrolls, and it is `.code-editor-scroll`. Everything else is laid out inside it at
   full content size and moves because that element moved. This is the whole of the fix for two
   faults reported from use: a gutter that grew the box to the height of the file instead of
   scrolling with it, and line numbers that fell progressively further behind the text — 72 sitting
   beside a 12 — because the previous version copied one element's scroll position onto another
   that had a different amount to scroll and quietly hit its own ceiling.

   Nothing is copied now. Three layers, one scroll position, nothing to keep in step.

   Every metric shared by the gutter, the painted copy and the textarea is set once, on the three
   of them together, and never overridden on any one of them. A single differing pixel of padding
   or line height slides the colour off the text a screenful down. */
.code-editor {
    border: var(--tblr-border-width) solid var(--tblr-border-color);
    border-radius: var(--tblr-border-radius);
    background: var(--tblr-bg-forms);
    overflow: hidden;
}

.code-editor-readonly {
    background: var(--tblr-bg-surface-secondary);
}

/* The one scrolling element, in both directions. A maximum rather than a fixed height, so a short
   file gets a short box instead of a screenful of empty grey. */
.code-editor-scroll {
    max-height: 24rem;
    overflow: auto;
}

/* As wide as the longest line, so the horizontal scrollbar belongs to the whole editor — gutter
   included — rather than to the text column alone. */
.code-editor-body {
    display: flex;
    align-items: stretch;
    min-width: max-content;
}

/* The shared metrics. Nothing else may set these three properties on any of these elements. */
.code-editor-gutter,
.code-editor-paint,
textarea.code-editor-input {
    font-family: var(--tblr-font-monospace);
    font-size: 0.8125rem;
    line-height: 1.45;
    tab-size: 4;
}

/* Stuck to the left edge, so scrolling a long line sideways does not carry the numbers off the
   screen with it. It scrolls vertically because it is inside the scrolling element, not because
   anything moves it. */
.code-editor-gutter {
    position: sticky;
    left: 0;
    z-index: 2;
    flex: 0 0 auto;
    padding: 0.5rem 0.5rem 0.5rem 0.75rem;
    text-align: right;
    color: var(--tblr-secondary);
    background: var(--tblr-bg-surface-tertiary);
    border-right: var(--tblr-border-width) solid var(--tblr-border-color);
    user-select: none;
}

/* Narrower than four digits would need, because these files do not run to ten thousand lines and a
   gutter sized for a number nobody will see is width taken from the text. */
.code-editor-line {
    min-width: 2ch;
}

/* The line the checker stopped at. A left bar rather than a background wash: the gutter is already
   a different colour from the text, and a second fill there reads as a selection. */
.code-editor-line-bad {
    color: var(--tblr-danger);
    font-weight: 700;
    box-shadow: inset 2px 0 0 0 var(--tblr-danger);
}

/* The text column. Its height comes from the painted copy inside it, which is in normal flow — so
   the column is exactly as tall as the file, and the scrolling element above has something to
   scroll. */
.code-editor-code {
    position: relative;
    flex: 1 1 auto;
    min-width: 0;
}

.code-editor-paint {
    margin: 0;
    padding: 0.5rem 0.75rem;
    white-space: pre;
    overflow: visible;
    background: none;
    border: 0;
    color: var(--tblr-body-color);

    /* Never clipped and never scrolled: this is what the box is sized from. */
    min-height: 100%;
}

/* The character ruler: ten characters of the text's own face, laid out but never seen, so one
   character's width can be measured instead of assumed. `visibility: hidden` rather than
   `display: none`, because a box that is not laid out has no width to read. Taken out of the flow
   so it cannot add to the column's height. */
.code-editor-ruler {
    position: absolute;
    top: 0;
    left: 0;
    visibility: hidden;
    pointer-events: none;
    white-space: pre;
    font-family: var(--tblr-font-monospace);
    font-size: 0.8125rem;
    line-height: 1.45;
}

/* Laid over the painted copy at exactly its size, and deliberately NOT scrollable itself — the one
   scrolling element is the ancestor. `resize: none` for the same reason: a corner grip here would
   resize the field out of register with the copy behind it.

   Qualified `textarea.` deliberately: the theme sets `textarea.form-control { min-height: … }`,
   which outranks a bare class, so the field would open two rows tall. */
textarea.code-editor-input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    min-height: 0;
    margin: 0;
    padding: 0.5rem 0.75rem;
    border: 0;
    border-radius: 0;
    resize: none;
    white-space: pre;
    overflow-wrap: normal;
    overflow: hidden;
    background: transparent;

    /* The text itself is invisible; the painted copy underneath is what is read. The caret is
       drawn from the body colour so it stays visible against either theme. */
    color: transparent;
    caret-color: var(--tblr-body-color);
}

textarea.code-editor-input:focus {
    outline: 0;
    box-shadow: none;
    background: transparent;
    color: transparent;
}

/* Selection has to stay legible over text that is not there. A translucent highlight lets the
   painted copy show through it. */
textarea.code-editor-input::selection {
    background: rgba(var(--tblr-primary-rgb), 0.28);
    color: transparent;
}

/* The token colours. Class names come from the highlighter; the values are the theme's, so the
   editor follows a light or dark switch with everything else rather than carrying its own
   palette. */
.code-editor-paint .token.property {
    color: var(--tblr-primary);
}

.code-editor-paint .token.string {
    color: var(--tblr-green);
}

.code-editor-paint .token.number,
.code-editor-paint .token.boolean,
.code-editor-paint .token.null {
    color: var(--tblr-orange);
}

.code-editor-paint .token.punctuation,
.code-editor-paint .token.operator {
    color: var(--tblr-secondary);
}

/* Comments are legitimate content in these files, not leftovers — one of them is how a spare
   server address is kept beside the live one. Dimmed and italic so they read as annotation, never
   hidden. */
.code-editor-paint .token.comment {
    color: var(--tblr-secondary);
    font-style: italic;
}

/* The accepted-file-types picker on the Application Directory form. Twenty-odd checkboxes, which
   is a page and a half without a bound. */
.media-type-picker {
    max-height: 16rem;
    overflow-y: auto;
}

/* The two name columns on Manage Application Files. The table shares the row with the upload form,
   so seven columns compete for about half the page; left to itself the browser gives the file name
   whatever is left after the three action buttons have taken theirs, and a name like
   `innosetup-7.1.0-x64.exe` wraps every five characters into an unreadable ribbon. A floor on the
   two name columns pushes the table into its own horizontal scroll instead, which is what
   .table-responsive around it is already there for. */
.application-file-name {
    min-width: 12rem;
}

/* ------------------------------------------------------------------------------------------------
   Media Management.

   Everything here exists because the pages show pictures, and the theme has no picture component.
   No rule below sets a colour or a font of its own — they are all sizes, ratios and containment,
   so the theme still decides how the page looks.
   ------------------------------------------------------------------------------------------------ */

/* The thumbnail area of a library card. A fixed 16:9 box rather than one that fits the image:
   without it a grid of banners and full-screen stills is a ragged column of different heights, and
   the eye cannot run down it. Portrait media letterboxes inside, which is honest — it is portrait,
   and the card should say so. */
.media-thumb {
    position: relative;
    aspect-ratio: 16 / 9;
    background: var(--tblr-bg-surface-secondary);
    overflow: hidden;
    border-top-left-radius: var(--tblr-border-radius);
    border-top-right-radius: var(--tblr-border-radius);
}

.media-thumb-image {
    width: 100%;
    height: 100%;
    /* `contain`, not `cover`: a cropped thumbnail of a banner hides the half of it that carries the
       message, and the operator is looking at these to recognise which file is which. */
    object-fit: contain;
}

.media-thumb-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    font-size: 3rem;
    color: var(--tblr-secondary);
}

/* A library card that will not reach a screen — switched off, or every schedule under it has
   ended. Dimmed as a whole and its picture drained of colour, so the live entries are the ones
   that catch the eye when the grid is scanned; the card's own badge says which of the two states
   it is in. */
.media-card-off {
    opacity: 0.65;
}

.media-card-off .media-thumb {
    filter: grayscale(1);
}

/* An operator's note on a card is clipped to two lines so a paragraph-length description does not
   make one card twice the height of its neighbours. The full text is on the edit page. */
.media-card-desc {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.media-thumb-badges {
    position: absolute;
    top: 0.5rem;
    left: 0.5rem;
    display: flex;
    gap: 0.25rem;
}

/* The preview dialog's contents. Bounded by the viewport rather than by the image, so a 4K still
   does not open a dialog taller than the screen. */
.media-preview-content {
    max-width: 100%;
    max-height: 70vh;
}

.media-preview-body {
    min-height: 8rem;
}

/* The drop zone on the upload form. A dashed outline is the one visual convention people already
   read as "you can drop something here"; `is-dragging` is added by the page script while a file is
   over it. */
.media-dropzone {
    border: 2px dashed var(--tblr-border-color);
    border-radius: var(--tblr-border-radius);
    padding: 1.25rem;
    text-align: center;
    transition: border-color 0.15s ease-in-out, background-color 0.15s ease-in-out;
}

.media-dropzone.is-dragging {
    border-color: var(--tblr-primary);
    background: var(--tblr-bg-surface-secondary);
}

.media-dropzone-icon {
    font-size: 2rem;
    color: var(--tblr-secondary);
}

.media-dropzone-text {
    color: var(--tblr-secondary);
}

/* The thumbnail beside the upload summary, the schedule form's media panel and the mapping page's
   header. One size for all three so the same picture is the same size wherever it appears. */
.upload-thumb {
    width: 6rem;
    height: 4rem;
    object-fit: contain;
    background: var(--tblr-bg-surface-secondary);
    border-radius: var(--tblr-border-radius);
    flex: 0 0 auto;
}

/* The thumbnail in a schedules table row. Small enough to leave the row one line tall. */
/* The still frame at the start of a schedule's row.

   min-width matters as much as width here, and is not belt-and-braces. The column it sits in is
   sized as a percentage — "as narrow as its contents" — while the theme caps every image in a
   table at the width of its cell. Those two rules chase each other: the cell asks the picture how
   wide it is, the picture answers "whatever the cell is", and the browser settles the argument at
   zero. A floor the percentage cannot argue with is what stops the picture disappearing. */
.schedule-thumb {
    width: 3.5rem;
    min-width: 3.5rem;
    max-width: 3.5rem;
    height: 2.25rem;
    object-fit: contain;
    background: var(--tblr-bg-surface-secondary);
    border-radius: var(--tblr-border-radius);
}

/* A caption can run to two thousand characters. Bounded so one long one does not push every other
   column off the table. */
.schedule-caption {
    max-width: 18rem;
}

/* A row of icon-only controls in a table.

   The theme's icon-button class removes the horizontal padding and leaves the width to whatever
   is inside, which for a bare glyph is fourteen pixels — narrower than a fingertip, and narrower
   than anything a pointer can be expected to hit on the first try. Squaring them off here keeps
   every control in the row the same size and comfortably large to aim at, and the larger glyph is
   what makes the difference between one icon and the next readable at a glance. The gap between
   them is tightened through the list variable rather than overridden, so six controls still fit a
   row without the table scrolling sideways. */
.table .btn-list {
    --tblr-list-gap: .25rem;
    align-items: center;
}

.table .btn-list .btn-icon {
    width: 2.25rem;
    min-width: 2.25rem;
    height: 2.25rem;
    font-size: 1.125rem;
}

/* A word and a glyph in the same row of controls are the same height.

   The rule above squares the icon buttons off at 2.25rem. A written control is sized by the
   theme's small padding instead, which is shorter, so a row holding both reads as two different
   sets of controls sitting at two different heights - and the destructive one at the end stops
   lining up with the rest, which is the one thing its position is meant to guarantee. Some rows
   need a word rather than a glyph, so the two have to agree on a height.

   Set on every control in the list rather than only on the written ones: whichever kind is added
   next inherits it, and no page has to remember. This is also why it is a minimum rather than a
   height - a control whose label wraps on a narrow screen grows instead of clipping. */
.table .btn-list .btn {
    min-height: 2.25rem;
}

/* A long list scanned down a column rather than read row by row.

   Only the cells' top and bottom padding is taken in. The side padding stays as it is, so the
   columns keep their spacing, and the row controls keep the height set above: with controls in
   the row, they are what sets its height, and a row too short for them is a row that jumps when
   they appear. */
.table.table-snug > :not(caption) > * > * {
    padding-top: .375rem;
    padding-bottom: .375rem;
}

/* A table heading of more than one word.

   Headings do not wrap by default, so a two-word heading over a one-word column takes as much of
   the table as the words are wide - two of them side by side took a quarter of the user list for
   four words. These may use a second line instead. */
.table th.th-wrap,
.table th.th-wrap a {
    white-space: normal;
}

/* The same, where every value in the column is a single badge: without a width the heading is
   still what sizes the column, so it is named and the heading wraps inside it. */
.table th.col-badge {
    width: 5rem;
}

/* A cell holding a moment in time, rewritten into the reader's own zone.

   The rewritten text is a date, a time to the second and a zone name — around thirty characters,
   which is the widest thing in most of these tables. Held on one line it pushes the row's controls
   off the side of the screen, so it is allowed to wrap; left to wrap freely in whatever width is
   left over it becomes four short lines and every row in the table is twice as tall as it needs to
   be. This is the floor that keeps it to two: the date, then the time. */
.table td.col-timestamp {
    min-width: 8.5rem;
}

/* A cell holding free text a device reported. The text may break anywhere (below), and that is
   what lets a table squeeze it: with no floor of its own, a narrow screen gives the column the
   width of a single letter and every word is cut in half. This floor keeps ordinary words whole;
   the table scrolls sideways inside its frame instead. */
.table td.col-remarks {
    min-width: 12rem;
}

/* A short status label in a badge reads as one thing; split over two lines it reads as two. */
.table td .status {
    white-space: nowrap;
}

/* Free text that may span several lines, such as a device's own report of its state.

   The text arrives as text and is placed as text; its line breaks are real newline characters,
   which the default whitespace handling would fold into spaces. Kept on a span inside the cell
   rather than on the cell itself, so the indentation around the value in the page source is not
   shown as blank lines. A device message can carry a long unbroken token - a path, a hex dump -
   so it may break anywhere rather than widen the table. */
.text-lines {
    white-space: pre-line;
    overflow-wrap: anywhere;
}

/* The phone field's country selector wraps the input in an inline-block of its own, which stops
   at the input's default character width instead of filling the column. Every other field on the
   same form fills its column, and a phone box two-thirds the width of the box above it reads as a
   mistake rather than as a choice. */
.iti {
    width: 100%;
}

/* One entry in the notification feed.

   The feed is read by scanning it: an operator arrives wanting to know what has happened lately,
   and the useful thing is how many entries fit on the screen at once. The theme's list item is
   sized for a menu of a handful of choices, so every entry took a full inch of height for three
   short lines of text. This tightens the padding and pulls the lines together, which roughly
   doubles what fits without making any single entry harder to read.

   The subject and the message both wrap. Neither is truncated: a message cut off at the width of
   the pane is a message somebody has to open the entry to finish reading, which is the opposite
   of what the feed is for. */
.notification-item {
    padding: .5rem .75rem;
    line-height: 1.35;
}

/* The dot between the details on an entry's last line. Dimmer than the text it separates, so the
   line reads as three things rather than as five. */
.notification-item .separator {
    opacity: .45;
}

/* The devices a kiosk has reported, opened out under its row on the status board.

   Set apart from the board rather than continuing it. Without a ground of its own the inner table
   reads as more rows of the outer one, and the reader loses which kiosk they opened - the row
   above scrolls away and nothing on screen says whose devices these are. A recessed panel, one
   step darker than the card, is what makes it read as a detail belonging to the row above rather
   than as part of the list. */
.device-panel {
    background: var(--tblr-bg-surface-secondary);

    /* The tint alone is one step off the card and reads as a printing artefact rather than as a
       boundary. A hairline top and bottom is what says where the detail starts and stops - drawn
       inside, because the cell it fills has no border of its own to lean on. */
    box-shadow:
        inset 0 1px 0 var(--tblr-border-color),
        inset 0 -1px 0 var(--tblr-border-color);
}

/* The inner table sits directly on that panel, so it must not paint the card's own ground back
   over it, and its outer edges have no border to draw against. */
.device-panel > .table {
    --tblr-table-bg: transparent;
}

.device-panel > .table > :not(caption) > * > * {
    background: transparent;
}

/* ---------------------------------------------------------------------------------------------
   The notification feed and its preview pane.

   Where the two stand side by side, both are sized to end at the bottom of the window, and each
   scrolls inside itself: the rows scroll under the level buttons with the pager kept in view below
   them, and the email scrolls inside its frame. The page itself then barely scrolls at all, and
   the preview is on screen for a row chosen anywhere in the feed. The page script measures where
   the pair starts, because the filter above them wraps to a different height at different widths;
   without that measurement the fallback is the whole window less a gutter. The minimum keeps a
   usable pane on a very short window, where the page scrolling again is the lesser evil.

   Narrower than this the columns stack, the pane follows the feed in the flow, and the frame takes
   most of the window's height instead; the page script brings it into view when a row is chosen.
   --------------------------------------------------------------------------------------------- */
.notification-preview-frame {
    display: block;
    height: 70vh;
    min-height: 20rem;
}

@media (min-width: 992px) {
    .notification-panels > [class*="col-"] > .card {
        display: flex;
        flex-direction: column;
        height: calc(100vh - var(--notification-panel-top, 1rem) - 1rem);
        min-height: 28rem;
    }

    .notification-panels #notification-items {
        flex: 1 1 auto;
        min-height: 0;
        overflow-y: auto;
    }

    .notification-panels #notification-email-body {
        display: flex;
        flex: 1 1 auto;
        flex-direction: column;
        min-height: 0;
    }

    .notification-panels .notification-preview-frame {
        flex: 1 1 auto;
        height: auto;
        min-height: 12rem;
    }
}

/* ---------------------------------------------------------------------------------------------
   The permission matrix: a group heading over the pages beneath it.

   Two levels have to read as two levels. The heading is set a step larger and heavier than the
   card's other headings, and the pages under it are stepped in from the left, so the eye reads the
   hierarchy the way the side menu draws it - a group, then its pages indented beneath - rather than
   a flat list in which one row happens to be bold. A page at the top level uses the heading style
   without the indent, because that is where it sits in the side menu too.
   --------------------------------------------------------------------------------------------- */
.permission-matrix .matrix-group {
    font-size: 1rem;
    font-weight: var(--tblr-font-weight-bold);
    letter-spacing: 0;
    text-transform: none;
}

/* Written against the card table's own first-column rule, which is more specific than a bare
   class pair and would otherwise keep the indent from applying. */
.card-table.permission-matrix tr td.matrix-page {
    padding-left: 2.75rem;
}

/* Stands where a box would be when a line does not take that level. A dash rather than a greyed
   box: a greyed box beside a live one is too faint to tell apart and still reads as grantable.
   Sized to the box's own width so the column stays aligned. */
.permission-matrix .matrix-level-unused {
    display: inline-block;
    width: 1rem;
    color: var(--tblr-secondary);
    line-height: 1;
    user-select: none;
}

/* ---------------------------------------------------------------------------------------------
   A hover panel that carries a list rather than a sentence.

   Opted into per element, so the ordinary one-line hover is untouched. The text arrives as text
   and is placed as text - the line breaks in it are real newline characters, not markup - so the
   only thing needed here is to stop them being collapsed the way the default whitespace handling
   collapses every run of space. Left-aligned for the same reason: a centred list of device names
   is much harder to scan down than a ragged-right one, and the width is bounded so a long device
   name wraps instead of stretching the panel across the window.
   --------------------------------------------------------------------------------------------- */
.tooltip-lines .tooltip-inner {
    white-space: pre-line;
    text-align: start;
    max-width: 22rem;
}

/* ---------------------------------------------------------------------------------------------
   The live view of a kiosk's screen.

   The frame is always exactly the picture's size - the page's script fits it, keeping the kiosk's
   proportions - and the picture and the pointer fill it, the pointer laid exactly over the picture.

   At the normal size the picture sits in a black stage under the status line, with the controls in
   the footer. Maximised, the dialog becomes the window: no header, no footer, the picture placed by
   the script edge to edge, the status floating over it, and the controls in a slim bar laid along the
   bottom or down the side, in the space the picture leaves.
   --------------------------------------------------------------------------------------------- */
.live-view-stage {
    position: relative;
    background: #000;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}

.live-view-frame {
    position: relative;
    flex: none;
    line-height: 0;
}

.live-view-picture,
.live-view-pointer {
    display: block;
    width: 100%;
    height: 100%;
}

.live-view-pointer {
    position: absolute;
    inset: 0;
    pointer-events: none;
}

/* While mouse control is on, the picture takes a crosshair: a click there is a click on the kiosk. */
.live-view-frame.is-controlled {
    cursor: crosshair;
}

.live-view-body {
    position: relative;
}

/* The warning before mouse control is switched on, laid over the picture at either size. */
.live-view-confirm {
    position: absolute;
    z-index: 3;
    top: 50%;
    left: 50%;
    width: min(28rem, calc(100% - 2rem));
    padding: 1.25rem;
    border-radius: var(--tblr-border-radius-lg, 0.5rem);
    transform: translate(-50%, -50%);
    background: var(--tblr-bg-surface, #fff);
    color: var(--tblr-body-color, #1d273b);
    box-shadow: 0 0.5rem 2rem rgba(0, 0, 0, 0.45);
}

.live-view-controls {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    min-width: 0;
}

.live-view-kiosk {
    display: none;
    min-width: 0;
}

.live-view-elapsed {
    margin-left: auto;
    font-variant-numeric: tabular-nums;
}

.live-view-screen .live-view-label,
.live-view-end .live-view-label {
    margin-left: 0.375rem;
}

/* ---- Maximised ------------------------------------------------------------------------------- */

.live-view-maximised .modal-header,
.live-view-maximised .modal-footer,
.live-view-maximised .live-view-detail {
    display: none;
}

.live-view-maximised .modal-content {
    background: #000;
    border: 0;
    border-radius: 0;
}

.live-view-maximised .live-view-body {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    padding: 0;
    overflow: hidden;
    background: #000;
}

.live-view-maximised .live-view-stage {
    position: absolute;
    inset: 0;
    display: block;
}

.live-view-maximised .live-view-frame {
    position: absolute;
}

.live-view-maximised .live-view-status {
    position: absolute;
    z-index: 2;
    top: 0.75rem;
    left: 50%;
    max-width: calc(100% - 2rem);
    margin: 0;
    padding: 0.375rem 0.875rem;
    border-radius: 999px;
    transform: translateX(-50%);
    background: rgba(0, 0, 0, 0.7);
    color: #fff;
    font-size: 0.875rem;
    pointer-events: none;
    transition: opacity 0.4s ease;
}

.live-view-maximised .live-view-status.is-quiet {
    opacity: 0;
}

.live-view-dock {
    position: absolute;
    z-index: 2;
    display: flex;
    background: rgba(18, 28, 43, 0.92);
    color: #fff;
}

.live-view-dock.is-bottom {
    left: 0;
    right: 0;
    bottom: 0;
    height: 56px;
    padding: 0 0.75rem;
    align-items: center;
}

.live-view-dock.is-side {
    top: 0;
    right: 0;
    bottom: 0;
    width: 72px;
    padding: 0.75rem 0;
    justify-content: center;
}

.live-view-dock .live-view-kiosk {
    display: block;
    flex: 1 1 auto;
    font-weight: 600;
}

.live-view-dock .live-view-elapsed {
    color: rgba(255, 255, 255, 0.7) !important;
}

.live-view-dock .btn-ghost-secondary {
    color: #fff;
}

/* Down the side the controls stand in a column, End at the foot, and say what they do by their
   icons and their labels for assistive technology - there is no room for words. */
.live-view-dock.is-side .live-view-controls {
    flex-direction: column;
    height: 100%;
}

.live-view-dock.is-side .live-view-kiosk,
.live-view-dock.is-side .live-view-label {
    display: none;
}

.live-view-dock.is-side .live-view-elapsed {
    margin-left: 0;
    margin-top: auto;
    font-size: 0.75rem;
}

.live-view-dock.is-side .live-view-end {
    padding-left: 0.625rem;
    padding-right: 0.625rem;
}

/* On a phone the bar along the bottom has no room for words either. */
@media (max-width: 575.98px) {
    .live-view-dock .live-view-label {
        display: none;
    }

    .live-view-dock .live-view-end {
        padding-left: 0.625rem;
        padding-right: 0.625rem;
    }
}

/* ---------------------------------------------------------------------------------------------
   A kiosk's page.

   The summary at the top is a grid of eight or nine short facts. Its columns are narrower than the
   ordinary grid's so that they fall into even rows — five across a wide screen, three on a tablet —
   rather than four and a fact left on a line of its own. On a phone the ordinary width gives each
   fact a line to itself, which makes the summary taller than the screen; narrowed, two sit side by
   side, which is as many as a phone has room for with a date in one of them.
   --------------------------------------------------------------------------------------------- */
.kiosk-summary-grid {
    --tblr-datagrid-item-width: 12rem;
}

@media (max-width: 575.98px) {
    .kiosk-summary-grid {
        --tblr-datagrid-item-width: 8.5rem;
    }

    /* The facts a phone shows only once More details is pressed, so the tabs are on the first
       screen. */
    .kiosk-summary-grid:not(.is-expanded) .kiosk-fact-more {
        display: none;
    }

    /* A kiosk's answer under each sent command is cut to fit the phone's width rather than
       pushing the table past it; the whole answer is in its tooltip. */
    .kiosk-sent-table .command-answer {
        max-width: 10rem;
    }
}

/* The tabs under the summary stay on one line at every width. Where they do not fit, the strip
   scrolls sideways under the reader's finger instead of breaking onto a second row, which would
   put a tab on a line of its own and make it read as a heading. */
.kiosk-tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
}

.kiosk-tabs::-webkit-scrollbar {
    display: none;
}

/* Sending a command and maintenance share a line on a wide screen, and each takes the full width
   below it. Side by side, the command card keeps the one width its widest command needs — the
   command list, a date range, a further choice and the button — and grows no further, because its
   fields are sized as fractions of it and a wider card only leaves an empty strip at the end. The
   maintenance card takes everything else, which is room for its tick boxes beside the duration. */
@media (min-width: 1700px) {
    .row > .kiosk-command-col {
        flex: 0 0 62rem;
        width: auto;
    }

    .row > .kiosk-maintenance-col {
        flex: 1 1 0;
        width: auto;
        min-width: 0;
    }
}

/* ---------------------------------------------------------------------------------------------
   What a report covers, above its table: one short fact per filter, and a last line naming the
   branches it reaches, which spans the grid because it can name many. A tinted panel, so it reads
   as a statement about the table rather than as more of the filter row above it.
   --------------------------------------------------------------------------------------------- */
.report-criteria {
    padding: 0.75rem 1rem;
    background: var(--tblr-bg-surface-secondary);
    border-radius: var(--tblr-border-radius);
}

.report-criteria .datagrid {
    --tblr-datagrid-padding: 0.75rem 1.25rem;
    --tblr-datagrid-item-width: 10rem;
}

.report-criteria .datagrid-item.is-wide {
    grid-column: 1 / -1;
}

/* The dates say why they differ from what was chosen, which takes a sentence: two columns where
   the grid has room for them, rather than a sentence folded into a narrow one. On a phone the
   short facts sit two to a row, and the dates take a row of their own. */
@media (min-width: 576px) {
    .report-criteria .datagrid-item:first-child:not(.is-wide) {
        grid-column: span 2;
    }
}

@media (max-width: 575.98px) {
    .report-criteria .datagrid {
        --tblr-datagrid-item-width: 8rem;
    }

    .report-criteria .datagrid-item:first-child {
        grid-column: 1 / -1;
    }
}

/* ---------------------------------------------------------------------------------------------
   The reports dashboard.

   The colours a chart draws in are stated here and read by the page's script, so the script names
   a role and never a colour. The four series colours are one fixed order, checked as a set for
   readers who see red and green alike: a chart with several series takes them from the first in
   this order and never skips one. The grey is for "everything else" beside one series in colour,
   and is grey on purpose. The rest follows the theme.
   --------------------------------------------------------------------------------------------- */
.sergas-dashboard {
    --dashboard-series-1: #2a78d6;
    --dashboard-series-2: #eb6834;
    --dashboard-series-3: #1baf7a;
    --dashboard-series-4: #eda100;
    --dashboard-rest: #898781;
    --dashboard-surface: var(--tblr-bg-surface);
    --dashboard-grid: var(--tblr-border-color);
    --dashboard-ink: var(--tblr-secondary);
}

/* A chart draws into the box it is given, so the box has a height of its own. A chart with a bar
   per item is given more by the script as the number of items asks for. */
.sergas-dashboard .dashboard-chart {
    position: relative;
    height: 16rem;
}

.sergas-dashboard .dashboard-chart.is-wide {
    height: 18rem;
}

/* A choice that belongs to one chart sits in that chart's header, at one width whichever option is
   chosen. On a narrow screen the header wraps and the choice moves under the title. */
.sergas-dashboard .dashboard-card-choice {
    width: 15rem;
    max-width: 100%;
}

/* A headline figure: the number, then a line that says what it is against. The line keeps its
   height while the figure loads, so the row of tiles does not jump when it arrives. */
.sergas-dashboard .dashboard-tile-value {
    font-variant-numeric: normal;
    line-height: 1.2;
}

.sergas-dashboard .dashboard-tile-detail {
    min-height: 2.5em;
}

/* The terminal's reasons for refusing a card: a short ranked list with the count at the end of
   each line, the reason allowed to wrap. */
.sergas-dashboard .dashboard-ranked li {
    display: flex;
    gap: 0.75rem;
    justify-content: space-between;
    padding: 0.375rem 0;
    border-bottom: 1px solid var(--tblr-border-color);
}

.sergas-dashboard .dashboard-ranked li:last-child {
    border-bottom: 0;
}

.sergas-dashboard .dashboard-ranked .dashboard-ranked-count {
    flex: 0 0 auto;
    font-variant-numeric: tabular-nums;
}
