:root { /* Shared with the toast offset below, so the two cannot drift apart. */ --navbar-height: 3.4em; } /* Nav bar: a hand-rolled flexbox bar. The user-chosen background and the foreground measured against it are set inline (see themes/navColors); this stylesheet only supplies structure and hover affordances. */ .wrolpi-navbar { display: flex; align-items: stretch; /* * Wraps rather than clipping. With this many items the bar overflows a 1000px * viewport, and `nowrap` cut "Admin" off the right edge -- the one link a user needs * to reach the settings that fix a broken WROLPi. */ flex-wrap: wrap; min-height: var(--navbar-height); padding: 0 0.25em; } /* * Toasts start below the navigation bar instead of on top of it. * * They are fixed at `top: var(--mantine-spacing-md)` -- 16px -- while the bar occupies the * first 54px of the page, so every toast landed over the bar's right-hand corner: the search * glyph, the hamburger, Help and Admin. For as long as the toast was up those controls could * not be clicked, and an error toast is exactly when a user reaches for Admin. * * It went unnoticed until the search glyph was given a proper hit area: at 18px it sat in the * gap under the toast, and growing it to the height of the bar moved it into the overlap. * The toast was always covering that corner; there was just nothing there to hit. * * `:where()` gives Mantine's own rule zero specificity, so one class is enough to win, but * the attribute is matched too -- only the top-anchored positions need moving, and a future * switch to `bottom-right` should not silently inherit a top offset. */ html .mantine-Notifications-root[data-position^='top'] { top: calc(var(--navbar-height) + var(--mantine-spacing-md, 1rem)); } .wrolpi-navbar-right { display: flex; align-items: center; margin-left: auto; gap: 0.2em; } .wrolpi-navbar-link, .wrolpi-navbar-link-button { display: flex; align-items: center; /* Tightened from 1.1em: the bar has eleven links plus nine indicators. */ padding: 0 0.8em; color: inherit; text-decoration: none; white-space: nowrap; background: none; border: none; font: inherit; cursor: pointer; } .wrolpi-navbar-link:hover, .wrolpi-navbar-link-button:hover { /* A translucent black works on every navbar color, light or dark. */ background: rgba(0, 0, 0, 0.12); } /* * The tab for the section the user is in. * * react-router marks it with `active` and `aria-current` on its own, and always did -- the * mark reached the DOM and nothing here drew it, so every tab looked identical and clicking * Videos appeared to leave the bar untouched. * * The underline is a pseudo-element in `currentColor`, not a border: `currentColor` follows * the foreground the bar computed for whatever navbar color the user picked, so this works * on all twelve of them (see `navColorNames`) and in all four themes without naming a color; * and a pseudo-element adds no height, where a border would jog the whole row as the mark * moved between tabs. The darker wash is the hover treatment deepened, so the two read as * the same idea. */ .wrolpi-navbar-link.active { position: relative; background: rgba(0, 0, 0, 0.2); } .wrolpi-navbar-link.active::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; /* rem, not px, so the mark grows with the --ui-scale setting like the text above it. */ height: 0.1875rem; background: currentColor; } /* * The same tabs inside the overflow "More" menu and the mobile hamburger, which are the * only place most of them appear on a phone. Those render in a portal, outside the bar, * so they take theme tokens rather than the bar's foreground. */ .mantine-Menu-item.active { background: var(--head); font-weight: 600; } /* * Everything in the bar takes the bar's foreground. * * `color: inherit` on the links alone was not enough, because two of the things in the * corner do not inherit at all and both came out blue on every bar in every theme: * * - An anchor takes `a {color: var(--blue)}` from tokens.css, which outranks an inherited * value however the bar sets it. The search glyph is an anchor, and so is every status * icon that links to /admin -- those only looked right because they carry an inline * color of their own, which still wins here and is meant to. * - A Mantine ActionIcon paints `--ai-color`, its own themed blue. The hamburger and the * theme toggle are ActionIcons. The variable is no use as a hook: Mantine writes it * INLINE on the element, so a stylesheet declaring it loses every time (the same trap * as `--group-justify` on Pagination). The painted property has to be set instead, and * `html` lifts this over Mantine's own single-class rule. * * Measuring the foreground against the bar is worth nothing if the icons opt out of it. */ .wrolpi-navbar a, html .wrolpi-navbar .mantine-ActionIcon-root { color: inherit; } /* The bar's own hover, rather than a blue tint from Mantine's palette. */ html .wrolpi-navbar .mantine-ActionIcon-root:hover { background: rgba(0, 0, 0, 0.12); color: inherit; } /* * One status indicator's slot. Laid out rather than nudged: see NavIconWrapper in Nav.js * for the 0.8em top margin this replaces and why it looked correct on half the indicators. */ .wrolpi-navbar-icon { display: flex; align-items: center; /* A sliver only: the corner's gap and the slots' own padding already separate the icons, and anything more has to fit a 320px phone times the number of slots. */ margin-left: 0.25em; /* Full bar height, so the anchor inside has somewhere to stretch to. */ align-self: stretch; } /* * An icon-only trigger in the corner gets a link's hit area, not its glyph's. * * The share glyph already computed `cursor: pointer` -- it is an `` -- but the * target was the 24px box the icon occupies, so the pointer appeared only if you found it * exactly, and there was no hover highlight to aim at. Stretched and padded, it behaves * like Help and Admin beside it. * * `align-self` rather than `align-items: stretch` on the wrapper: the IconButtons in the * corner have an explicit height, and a stretch container places an item it cannot stretch * at the start -- which would push the hotspot and theme glyphs back off centre. */ .wrolpi-navbar-icon > a { align-self: stretch; padding: 0 0.5em; cursor: pointer; } .wrolpi-navbar-icon > a:hover { background: rgba(0, 0, 0, 0.12); } /* * An anchor in the corner hugs its glyph instead of a text line box. * * A line box reserves descender space for text that is not there, which leaves an icon * sitting about 3px above the centre of the box being centred -- the share and search * glyphs, but not the hotspot or theme picker, which are square ActionIcons that centre * their own glyph. That difference is what made a single top margin look right on some * indicators and wrong on others. */ .wrolpi-navbar-right a { display: flex; align-items: center; } /* * A link in the corner fills the bar's height, as one outside it does. * * Help and Admin highlighted only a box around their text while Videos and Statistics * highlighted the full height. Same class and same rules; the difference is the parent. * The bar stretches its children, but the corner is a nested flex container that centres * its own, so a link inside it was sized to its content. The hover background is painted * on the link box, so this is the hover affordance itself and not just its outline. */ .wrolpi-navbar-right .wrolpi-navbar-link, .wrolpi-navbar-right .wrolpi-navbar-link-button { align-self: stretch; } ::-ms-clear { display: none; } pre.wrap-text { white-space: pre-wrap; /* Since CSS 2.1 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ } /* * A video's length, pinned to the corner of its poster. * * Tokens, not a literal white-on-black. The poster under it is filtered by night mode -- it is a * `.media` leaf -- but this chip is drawn beside the image rather than inside it, so it was never * filtered: a white tile on a card whose picture had gone dim red, which is the brightest thing on * the videos page in the theme meant to have nothing bright on it. * * Opaque, because it sits on an arbitrary poster and has to be legible over any of them. */ .duration-overlay { position: absolute; top: 0.25rem; right: 0.25rem; color: var(--text); background: var(--panel); border: 1px solid var(--border); padding: 0px 0.25rem; border-radius: 0.1875rem; } .no-link-underscore { text-decoration: none !important; } /* * A card's title and its author/channel/domain line are links, but they are the card's * text -- they take the body color rather than the link color, so a grid of cards reads * as titles and not as a wall of blue. This was a literal `black` until the themes * arrived, which left every title at about 1.1:1 on the dark, night and amber panels. */ .card-link { color: var(--text); } /* * The hand-rolled `.toggle` / `.slider` switch is gone. * * Nothing referenced either class: `Toggle` is a Mantine Switch, and the only remaining * matches for the words were the `classList.toggle` DOM method and the controller's own * templates, which do not load this stylesheet. * * It is deleted rather than finished because it was already half-converted and broken: the * track and knob became rem while the checked-state travel stayed `translateX(26px)`, so on a * 54px track the thumb would have stopped ~4px short. Dead CSS that is also wrong is a trap * for whoever revives it, and it also hardcoded #6435C9 and white, which no theme can reach. */ /* File preview Modal.Actions toolbar. Single layout that works at every viewport — buttons keep their order at any width. On narrow screens, the tags selector wraps to its own line while the button groups stay together. */ .preview-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5em; /* Fill the actions row: `.wrolpi-modal-actions` packs its children to the right, which is where a confirming button belongs but strands this toolbar. At full width the toolbar's own layout wins and everything reads from the left. */ flex: 1 1 100%; } .preview-toolbar-group { display: flex; align-items: center; gap: 0.25em; flex: 0 0 auto; } .preview-toolbar-tags { /* rem so the basis grows with the tags it holds. */ flex: 1 1 12.5rem; min-width: 0; } /* On a phone the tags selector always gets its own line rather than squeezing in beside the buttons. 700px is the app's `tablet` breakpoint (`AppMedia` in contexts.js). */ @media (max-width: 699px) { .preview-toolbar-tags { flex-basis: 100%; } } /* Caps preview content to fit inside the file-preview Modal. 300px reserves room for pathContent
, Modal.Header, Modal.Actions row,
   inner content (e.g. STL dimensions banner), and modal padding.
   Use 100dvh so mobile browser chrome (URL bar) is accounted for. */
.preview-fit {
    width: 100%;
    height: calc(100dvh - 18.75rem);
    max-height: calc(100dvh - 18.75rem);
    min-height: 15rem;
    overflow: hidden;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Fresnel (Media) wraps iframes in a div which would otherwise shrink to the
   default iframe size; make it fill the preview so the iframe's 100% works. */
.preview-fit .fresnel-container {
    width: 100%;
    height: 100%;
}

td.file-path {
    cursor: pointer;
}

/*
 * The row is an icon followed by a filename, as inline siblings.  An inline SVG
 * sits on the text baseline by default, so a 24px glyph next to 13px text hangs
 * well above it and the name looks like it is resting on the icon's floor.
 * `middle` centres the glyph against the text instead.
 *
 * The margin is the gap that was never there: the folder icon touched the first
 * letter of its own name, and two icons (a folder and its ignored marker) touched
 * each other.
 */
td.file-path > svg {
    vertical-align: middle;
    margin-right: 0.35em;
}

textarea.otp {
    width: 100%;
    color: #333;
}

.card-icon {
    min-height: 10.1875rem;
    text-align: center;
    padding-top: 3.3125rem;
}

.clickable {
    cursor: pointer;
}

/*
 * A table that truncates to the width it has rather than growing past it: the columns divide
 * the table's width, and `column-ellipsis` gives up what it cannot show.  A column that must
 * fit something exact states a width on its header cell; the rest is the ellipsis column's.
 *
 * `table.`, not the bare class: Mantine sets `table-layout` on its own `.mantine-Table-table`
 * at the same specificity and later in the cascade, so the bare class loses -- silently, with
 * every other property here still applying.
 */
table.table-ellipsis {
    width: 100%;
    table-layout: fixed;
}

.table-ellipsis td.column-ellipsis {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/*
 * Column widths for the downloads tables.  Phone first, and the phone is 375px: below the
 * breakpoint each column is cut to what it must hold -- its header's longest word, the label
 * wrapping, or its buttons -- and the URL takes what is left, which is little.  Above it the
 * labels get their own line back and the URL gets the rest.
 *
 * 700px is the app's `tablet` breakpoint (`AppMedia` in contexts.js).  `em` here is the
 * table's own font (~15.4px), not the root's.
 */
/*
 * A header too long for a phone carries both forms and shows one.  The short word is what the
 * mobile column widths below are sized for; the full words return at the breakpoint.
 */
.table-ellipsis th .label-long {
    display: none;
}

@media (min-width: 700px) {
    .table-ellipsis th .label-long {
        display: inline;
    }

    .table-ellipsis th .label-short {
        display: none;
    }
}

.table-ellipsis th.col-select {
    /* 22px of checkbox plus 11px of cell padding either side. */
    width: 3em;
}

.table-ellipsis th.col-frequency {
    width: 4.2em;
}

.table-ellipsis th.col-completed {
    width: 5.5em;
}

.table-ellipsis th.col-completed-plain {
    width: 4.2em;
}

.table-ellipsis th.col-next {
    width: 3.9em;
}

/* A `ButtonGroup` cannot wrap -- its buttons are joined -- so this holds all three of them
   (delete, edit, retry on a failed download) at every width. */
.table-ellipsis th.col-control-group {
    width: 9.5em;
}

/* A `.wrolpi-button-row` wraps, so on a phone this holds one button and the second drops
   beneath it.  It is sized by the word "Control" rather than by the button, which is wider. */
.table-ellipsis th.col-control-row {
    width: 5.2em;
}

@media (min-width: 700px) {
    .table-ellipsis th.col-frequency {
        width: 12em;
    }

    .table-ellipsis th.col-completed {
        width: 9.5em;
    }

    .table-ellipsis th.col-completed-plain {
        width: 8.5em;
    }

    .table-ellipsis th.col-next {
        width: 4em;
    }

    .table-ellipsis th.col-control-row {
        width: 7.5em;
    }
}

/*
 * A header wraps instead of running into the next column: `.wrolpi-th-sort` is nowrap, which a
 * fixed column cannot widen for, so a long label printed over its neighbour.
 *
 * `min-width: 0` as well as the wrap -- the label is an anonymous flex item whose automatic
 * minimum is its longest word, and without this it still refuses to go below that word.  A
 * column narrower than its longest word is a width to fix at the call site.
 */
.table-ellipsis th .wrolpi-th-sort {
    display: flex;
    white-space: normal;
    min-width: 0;
    text-align: left;
}

/*
 * A header is clipped rather than painted over its neighbour.  Wrapping cannot save a
 * single-word label in the elastic column, which may be narrower than the word itself.
 */
.table-ellipsis th {
    overflow: hidden;
}

/* `ButtonGroup` is a nowrap flex row whose children shrink; `col-control-group` is sized for
   the whole group, so the buttons in it must not. */
.table-ellipsis td .mantine-ButtonGroup-group > * {
    flex-shrink: 0;
}

.card-title-ellipsis {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
    text-overflow: ellipsis;
}

.search-container {
    display: flex;
    justify-content: flex-start;
}

.search-input {
    flex-grow: 1;
}

.video-player {
    display: block;
    margin: 0 auto; /* Center the video horizontally */
}

.audio-poster-container {
    position: relative;
    display: block;
    text-align: center;
}

.audio-poster-overlay {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: rgba(255, 255, 255, 0.85);
    text-shadow: 0 0 20px rgba(0, 0, 0, 0.6);
    pointer-events: none;
}

table.ratio-table td.equal-width {
    width: 48%;
}

table.ratio-table td.min-width {
    width: auto;
}

/*
 * Sticky footer for the file browser.
 *
 * The surface is a token.  It was two hardcoded greys with `.inverted` selecting the dark
 * one, and once nothing set `.inverted` any more the footer stayed at near-white `#f9fafb`
 * in dark, night and amber.  In night mode a white bar pinned to the bottom of the screen
 * costs the user the dark adaptation the whole theme exists to protect.  A token has no
 * such failure mode: there is one rule and each theme fills it in.
 */
.sticky-footer {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    padding: 0.5em 1em;
    background-color: var(--panel);
    border-top: 1px solid var(--border);
    z-index: 100;
    /*
     * A row with gaps, like every other group of buttons in the app.
     *
     * This was a plain block, so the buttons -- inline-block elements with no whitespace
     * between them, because JSX drops whitespace-only lines -- sat flush against each other
     * in one unbroken slab.  Everywhere else a group of buttons is a flex row with a gap;
     * the file browser's footer is hand-rolled and never got one.  It wraps because the bar
     * carries eight controls and a phone cannot hold them.
     */
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5em;
}

/*
 * Drag selection visual feedback for FileBrowser: a blue tint over the rows being taken,
 * red over the rows being dropped.
 *
 * Each had a second, stronger rule under `.inverted` for the dark themes.  Nothing has set
 * that class since ThemeProvider stopped supplying it, so only the light tint has been
 * drawn for some time -- and 15% of a fixed blue over a dark row is close to invisible,
 * which is the one moment the user needs to see what they have hold of.
 *
 * A token mixed with transparent instead: the hue follows the theme, and the strength can
 * go up without pinning it to a literal that only suits one background.
 */
tr.drag-selecting td {
    background-color: color-mix(in srgb, var(--blue) 22%, transparent) !important;
}

tr.drag-deselecting td {
    background-color: color-mix(in srgb, var(--red) 22%, transparent) !important;
}

/* Prevent text selection during drag */
.file-browser-dragging {
    user-select: none;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
}

/* Filter input and collapse-all button: right-aligned on desktop, full width on mobile (below). */
.file-browser-filter-container {
    display: flex;
    justify-content: flex-end;
    /* The gap is the container's.  The collapse-all button carried `margin-left: 0.5em` for it,
       which is the same thing one element to the right of where it belongs. */
    gap: 0.5em;
    /* No `margin-bottom`: this is a top-level block of the Files page, and the page's stack
       spaces it (see `.wrolpi-stack`).  Its own 0.5em was added to that. */
}

/*
 * Double the default input width on desktop.
 *
 * The selector was `.ui.input.file-browser-filter`, naming a wrapper the old input
 * component rendered around the field.  Nothing carries `ui input` any more, so this rule
 * and the mobile one below it had been matching nothing at all: the filter was rendering
 * at the browser's default width on a bar laid out to give it 34em.
 *
 * The class lands on ActionInput's wrapper, which is the field and its clear button
 * together -- that pair is the control being sized, not the text box alone.
 *
 * Qualified by the container so it carries two classes.  `.wrolpi-action-input` sets
 * `width: 100%` in ui.css, which is one class like `.file-browser-filter` is, and ui.css
 * is loaded after this file -- so a bare class selector here loses the tie and the field
 * fills the row anyway.  Specificity, not source order, is what should decide it.
 */
.file-browser-filter-container .file-browser-filter {
    width: 34em;
    max-width: 100%;
}

@media only screen and (max-width: 700px) {
    .hide-on-mobile {
        display: none !important;
    }

    .file-browser-filter-container {
        justify-content: flex-start;
    }

    /* Fill the row, but stay on one line with the collapse-all button. */
    .file-browser-filter-container .file-browser-filter {
        flex: 1 1 auto;
        width: auto;
    }
}

/* Inventory "PDF" export: the printable table is hidden on screen and becomes the only
   visible content when printing, so a user can use the browser dialog to Save as PDF. */
.inventory-print {
    display: none;
}

@media print {
    body * {
        visibility: hidden;
    }

    .inventory-print,
    .inventory-print * {
        visibility: visible;
    }

    .inventory-print {
        display: block;
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        color: #000;
    }

    .inventory-print h1 {
        font-size: 18pt;
        margin: 0 0 0.25em;
    }

    .inventory-print h2 {
        font-size: 13pt;
        margin: 1.25em 0 0.5em;
    }

    .inventory-print-meta {
        font-size: 9pt;
        color: #444;
        margin: 0 0 0.75em;
    }

    .inventory-print table {
        width: 100%;
        border-collapse: collapse;
        font-size: 10pt;
    }

    .inventory-print th,
    .inventory-print td {
        border: 1px solid #999;
        padding: 0.25rem 0.375rem;
        text-align: left;
        vertical-align: top;
    }

    /* Repeat the header row at the top of each printed page. */
    .inventory-print thead {
        display: table-header-group;
    }

    .inventory-print tr {
        page-break-inside: avoid;
    }

    /* The supply-plan shopping list reuses the .inventory-print element styling (h1/table/etc.) but is a separate
       printable block.  By default (e.g. the Export tab's whole-inventory PDF) it is hidden; it becomes the ONLY
       printed block when the supply plan toggles `body.printing-shopping` just before window.print(). */
    .shopping-print,
    .shopping-print * {
        visibility: hidden;
    }
    .shopping-print {
        display: none;
    }
    body.printing-shopping .inventory-print:not(.shopping-print),
    body.printing-shopping .inventory-print:not(.shopping-print) * {
        visibility: hidden;
    }
    body.printing-shopping .inventory-print:not(.shopping-print) {
        display: none;
    }
    body.printing-shopping .shopping-print,
    body.printing-shopping .shopping-print * {
        visibility: visible;
    }
    body.printing-shopping .shopping-print {
        display: block;
    }
}

/* Map popups (POI details and pins) follow the active theme's tokens. */
/* Scoped under .maplibregl-popup to outrank maplibre-gl.css, which loads after this file. */
.maplibregl-popup .maplibregl-popup-content {
    background: var(--panel);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 0.25rem;
}
.maplibregl-popup .maplibregl-popup-close-button {
    color: var(--muted);
}
.maplibregl-popup .maplibregl-popup-close-button:hover {
    background: var(--head);
    color: var(--text);
}
.maplibregl-popup.maplibregl-popup-anchor-top .maplibregl-popup-tip,
.maplibregl-popup.maplibregl-popup-anchor-top-left .maplibregl-popup-tip,
.maplibregl-popup.maplibregl-popup-anchor-top-right .maplibregl-popup-tip {
    border-bottom-color: var(--panel);
}
.maplibregl-popup.maplibregl-popup-anchor-bottom .maplibregl-popup-tip,
.maplibregl-popup.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip,
.maplibregl-popup.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip {
    border-top-color: var(--panel);
}
.maplibregl-popup.maplibregl-popup-anchor-left .maplibregl-popup-tip {
    border-right-color: var(--panel);
}
.maplibregl-popup.maplibregl-popup-anchor-right .maplibregl-popup-tip {
    border-left-color: var(--panel);
}