/*
 * Plain and functional, modelled on the WordPress admin area (D-018).
 * Only what Bootstrap does not already cover — no second design system.
 */

:root {
    /*
     * The frame — top bar and sidebar — is one surface, and it follows the
     * colour mode (D-107). It sits on the same colour as the cards, while the
     * work area keeps the tinted ground; so the frame reads as one piece and
     * the content sits inside it.
     *
     * This replaces the permanently dark bar from D-018. A frame that stays
     * dark while the top bar next to it turns white reads as two half-finished
     * ideas, and a colour mode that only reaches half the screen is not one.
     */
    --app-frame-bg: var(--bs-body-bg);
    --app-frame-fg: var(--bs-body-color);
    --app-frame-muted: var(--bs-secondary-color);
    --app-frame-hover: var(--bs-tertiary-bg);
    --app-frame-border: var(--bs-border-color);

    /* The link colour, not --bs-primary: primary is one value for both modes
       (#0d6efd), and on a dark ground that is too dark to read as "you are
       here". Bootstrap already keeps a lighter blue for links in dark mode
       (#6ea8fe), which is the colour every other link on the page has — so the
       marker matches them instead of inventing a shade of its own. */
    --app-frame-active: var(--bs-link-color);
    --app-frame-active-hover: var(--bs-link-hover-color);

    --app-sidebar-width: 200px;
    --app-sidebar-rail: 56px;

    /* One height for the top bar and for the name above the navigation, so the
       two lines under them are one line across the whole screen. */
    --app-bar-height: 3.25rem;

    /* The faint wash that lifts one cell out of a grid — today in the calendar,
       an entry under the pointer. Its own variable and not --bs-tertiary-bg,
       which is a shade too strong for something meant to be barely noticed. */
    --app-tint: #f6f7f7;
    --app-tint-warm: #fbfaf7;
}

/* Dark mode is Bootstrap's own (data-bs-theme, 5.3). Only the two washes above
   have to be restated — everything else in this file already reads a Bootstrap
   variable and follows along by itself.

   The sidebar deliberately stays dark in both modes: it is the frame around the
   work, and a frame that changes colour with its contents stops being one. */
[data-bs-theme='dark'] {
    --app-tint: #2b3035;
    --app-tint-warm: #32302b;
}

/* Bootstrap keeps the light-mode grey on outline buttons in dark mode, which
   lands near 3:1 against the dark background — under what 07-ui-ux.md asks for.
   These are "Suchen", "Heute", "Bearbeiten": real work, not decoration. */
[data-bs-theme='dark'] .btn-outline-secondary {
    --bs-btn-color: #adb5bd;
    --bs-btn-border-color: #6c757d;
    --bs-btn-hover-bg: #6c757d;
    --bs-btn-hover-border-color: #6c757d;
    --bs-btn-active-bg: #6c757d;
    --bs-btn-active-border-color: #6c757d;
    --bs-btn-disabled-color: #6c757d;
    --bs-btn-disabled-border-color: #495057;
}

body {
    font-size: 0.85rem;
}

/* Icons come out of the sprite that crm:build-icon-sprite writes. Sized in em
   so each one follows the text it sits beside, rather than needing a class for
   every place it turns up. */
.app-icon {
    width: 1em;
    height: 1em;
    flex: 0 0 auto;
    vertical-align: -0.125em;
}

/* Sidebar left, work area right. Below lg the sidebar becomes an offcanvas
   panel and the work area takes the full width. */
.app-shell {
    display: flex;
    min-height: 100vh;
}

.app-main {
    flex: 1 1 auto;
    min-width: 0; /* lets wide content shrink instead of pushing the page sideways */
}

/* !important is needed here: from the lg breakpoint on, Bootstrap sets
   `background-color: transparent !important` on .offcanvas-lg, which would
   otherwise let the tinted ground show through the bar. */
.app-sidebar {
    background-color: var(--app-frame-bg) !important;
    color: var(--app-frame-fg);
    border-right: 1px solid var(--app-frame-border);
}

@media (min-width: 992px) {
    .app-sidebar {
        flex: 0 0 var(--app-sidebar-width);
        width: var(--app-sidebar-width);
    }
}

.app-sidebar-brand {
    display: flex;
    align-items: center;
    height: var(--app-bar-height);
    padding: 0 1rem;
    font-weight: 600;
    border-bottom: 1px solid var(--app-frame-border);
}

/* The top bar and the sidebar stay put; only the work area scrolls. In a list
   of two hundred customers the column headings and the way out of the page
   would otherwise be somewhere above the top of the screen.

   sticky rather than fixed: the sidebar keeps its place in the flex row, so
   the work area needs no margin standing in for a width it cannot see. */
.app-topbar {
    position: sticky;
    top: 0;
    z-index: 1020;
    height: var(--app-bar-height);
    /* Same surface as the sidebar, so the two read as one frame rather than as
       two strips that happen to meet in the corner. */
    background-color: var(--app-frame-bg);
}

/* Everything in the bar to one height. A .btn that is also a flex container
   takes its height from its tallest item, so a button whose label is hidden
   comes out at 24 pixels next to a 31 pixel neighbour — which is exactly what
   happens on a phone, where the labels give way. Three heights stood side by
   side in the bar before this: 24, 31 and the bell's 52.

   The figure is Bootstrap's own btn-sm height written out, not a number picked
   by eye: line height 1.5, padding 0.25rem top and bottom, one pixel of border
   each side. */
.app-topbar .btn,
.app-topbar .form-control,
.app-topbar .form-select {
    min-height: calc(1.5em + 0.5rem + 2px);
}

@media (min-width: 992px) {
    /* Specific enough to beat Bootstrap's own .offcanvas-lg:not(.offcanvas),
       which sets position: static from this breakpoint up. */
    .app-sidebar.offcanvas-lg:not(.offcanvas) {
        position: sticky;
        top: 0;
        height: 100vh;
        overflow-y: auto;
    }
}

.app-sidebar .nav-link {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    color: var(--app-frame-fg);
    border-radius: 0;
    padding: 0.55rem 1rem;
}

.app-sidebar .nav-link:hover,
.app-sidebar .nav-link:focus-visible {
    background-color: var(--app-frame-hover);
    color: var(--app-frame-fg);
}

/* Without the dark bar, this stripe is the only thing saying where you are —
   so it keeps the colour, the tint and the edge all three. */
.app-sidebar .nav-link.active {
    background-color: rgba(var(--bs-link-color-rgb), 0.12);
    color: var(--app-frame-active);
    box-shadow: inset 3px 0 0 var(--app-frame-active);
}

/* Pointing at the entry you are already on brightens it the same way pointing
   at a link does. Without this the rule above would hand it the plain body
   colour on hover and the marker would go out under the pointer. */
.app-sidebar .nav-link.active:hover,
.app-sidebar .nav-link.active:focus-visible {
    background-color: rgba(var(--bs-link-color-rgb), 0.18);
    color: var(--app-frame-active-hover);
}

/* Held back a little, so sixteen of them read as a column of entries and not as
   a wall of pictures. The one you are standing on gets its full weight. */
.app-nav-icon {
    width: 1.1em;
    height: 1.1em;
    opacity: 0.7;
}

.app-sidebar .nav-link:hover .app-nav-icon,
.app-sidebar .nav-link:focus-visible .app-nav-icon,
.app-sidebar .nav-link.active .app-nav-icon {
    opacity: 1;
}

.app-nav-badge {
    margin-left: auto;
}

/* Section label above a group of entries — and, since the bar holds sixteen
   entries, the handle that folds the group away. Quiet on purpose: it
   separates, it is not the thing you came for. */
.app-nav-heading {
    padding: 0;
}

.app-nav-heading-toggle {
    display: flex;
    align-items: center;
    width: 100%;
    padding: 1.1rem 1rem 0.4rem;
    border: 0;
    background: none;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-align: left;
    color: var(--app-frame-muted);
}

.app-nav-heading-toggle:hover,
.app-nav-heading-toggle:focus-visible {
    color: var(--app-frame-fg);
}

/* Bootstrap adds .collapsed to the button while its panel is shut. */
.app-nav-chevron {
    margin-left: auto;
    transition: transform 0.15s ease-out;
}

.app-nav-heading-toggle.collapsed .app-nav-chevron {
    transform: rotate(-90deg);
}

@media (prefers-reduced-motion: reduce) {
    .app-nav-chevron {
        transition: none;
    }
}

/*
 * The bar narrowed to icons, for the calendar and the wide lists — it gives
 * them about 150 pixels back. Only from lg up: below that the bar is an
 * offcanvas panel and there is nothing to narrow.
 */
@media (min-width: 992px) {
    .has-rail .app-sidebar {
        flex-basis: var(--app-sidebar-rail);
        width: var(--app-sidebar-rail);
    }

    /* Hidden from the eye, not from the document: a screen reader still reads
       every entry by its name, and the title attribute answers the pointer. */
    .has-rail .app-nav-label {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .has-rail .app-sidebar .nav-link {
        justify-content: center;
        padding: 0.6rem 0;
    }

    .has-rail .app-nav-icon {
        width: 1.25em;
        height: 1.25em;
    }

    .has-rail .app-sidebar-brand {
        justify-content: center;
        padding: 0;
    }

    .has-rail .app-nav-heading-toggle {
        padding: 0.7rem 1rem 0.2rem;
        pointer-events: none; /* nothing left to read, nothing left to click */
    }

    .has-rail .app-nav-chevron {
        display: none;
    }

    /* A group somebody folded away would be unreachable once its heading is
       gone, so narrowing the bar opens all of them. */
    .has-rail .app-nav-group {
        display: block !important;
        height: auto !important;
    }

    .has-rail .app-sidebar .nav-item {
        position: relative;
    }

    /* The count turns into a dot. There is no room for the number, but "there
       is something waiting here" is exactly what must not get lost. */
    .has-rail .app-nav-badge {
        position: absolute;
        top: 0.3rem;
        right: 0.8rem;
        width: 0.5rem;
        height: 0.5rem;
        padding: 0;
        border-radius: 50%;
        overflow: hidden;
        text-indent: -100px;
    }
}

/* The button shows the direction the bar will move in, not the state it is in. */
.app-rail-narrow,
.has-rail .app-rail-wide {
    display: none;
}

.has-rail .app-rail-narrow {
    display: inline-block;
}

/* No frame on it: this sets a preference about the furniture, it does not do
   anything to the records — and a framed button beside the search field reads
   as though it did. Bootstrap's own variables rather than plain properties, so
   focus ring and disabled state keep working. */
.app-rail-toggle {
    --bs-btn-color: var(--bs-secondary-color);
    --bs-btn-bg: transparent;
    --bs-btn-border-color: transparent;
    --bs-btn-hover-color: var(--bs-body-color);
    --bs-btn-hover-bg: var(--bs-tertiary-bg);
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-color: var(--bs-body-color);
    --bs-btn-active-bg: var(--bs-tertiary-bg);
    --bs-btn-active-border-color: transparent;

    padding: 0.3rem 0.4rem;
}

/* Search in the top bar. The panel hangs below the field without pushing the
   page around, so the field does not jump while results come in. */
.app-search {
    position: relative;
    flex: 1 1 auto;
    max-width: 26rem;
}

/* The magnifier sits inside the field, not beside it: an input group would put
   a wrapper between .app-search and the input, and the results panel hangs off
   .app-search. Not clickable — the field behind it is what you want. */
.app-search-icon {
    position: absolute;
    top: 50%;
    left: 0.6rem;
    width: 0.85rem;
    height: 0.85rem;
    transform: translateY(-50%);
    color: var(--bs-secondary-color);
    pointer-events: none;
}

.app-search .form-control {
    padding-left: 1.85rem;
}

.app-search-results {
    position: absolute;
    z-index: 1030;
    top: calc(100% + 0.25rem);
    left: 0;
    right: 0;
    max-height: 70vh;
    overflow-y: auto;
    background-color: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 0.375rem;
    box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}

.app-search-group {
    padding: 0.5rem 0.75rem 0.25rem;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
}

.app-search-hit {
    display: block;
    padding: 0.4rem 0.75rem;
    color: inherit;
    text-decoration: none;
}

.app-search-hit:hover,
.app-search-hit:focus-visible,
.app-search-hit.active {
    background-color: var(--bs-tertiary-bg);
    color: inherit;
}

.app-search-hit-title {
    display: block;
}

.app-search-hit-subtitle {
    display: block;
    font-size: 0.8rem;
    color: var(--bs-secondary-color);
}

.app-search-more {
    font-size: 0.85rem;
}

.app-search-message {
    padding: 0.6rem 0.75rem;
    font-size: 0.85rem;
    color: var(--bs-secondary-color);
}

/* The bell in the top bar (D-146).

   The count sits on the corner of the button rather than beside it: the bar is
   crowded on a laptop, and the badge has to survive the search field growing.

   `.btn` in front of it because Bootstrap has `.btn .badge { position:
   relative; top: -1px }`, and two classes beat one. Written as `.app-bell-count`
   alone this rule lost every time and never did anything: the count stood in
   the flow inside the button, and on a narrow bar it wrapped onto a second
   line and made the button 52 pixels tall — taller than the bar itself.
   Measured on 25 August 2026, at 390 pixels. */
.btn .app-bell-count {
    position: absolute;
    top: -0.35rem;
    right: -0.35rem;
    font-size: 0.65rem;
    padding: 0.15rem 0.35rem;
}

/* Something arrived. Two or three beats and then still — WCAG 2.2.2 wants a way
   to stop anything that moves for more than five seconds, and the honest answer
   is not to move for that long. It is also the practical one: a badge that
   blinks all day stops being read by the second afternoon.

   Well under three flashes a second (WCAG 2.3.1), and the animation is on the
   ring rather than the number, so nothing that has to be read is moving. */
@keyframes app-bell-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(var(--bs-danger-rgb), 0.5); }
    50% { box-shadow: 0 0 0 0.35rem rgba(var(--bs-danger-rgb), 0); }
}

.app-bell-new [data-bell-toggle] {
    animation: app-bell-pulse 1s ease-out 3;
    border-radius: 0.375rem;
}

.app-bell-menu {
    width: min(24rem, calc(100vw - 1.5rem));
}

.app-bell-body {
    max-height: 60vh;
    overflow-y: auto;
}

.app-bell-entry {
    display: flex;
    gap: 0.5rem;
    padding: 0.5rem 0.75rem;
    color: inherit;
    text-decoration: none;
    border-bottom: 1px solid var(--bs-border-color);
}

/* Which kind of notification, taken from the navigation entry it leads towards
   (D-146). Quiet on purpose: it is a signpost, not a warning, and the sentence
   beside it is what has to be read. Colour tells nothing apart here — red,
   amber and green carry meaning in this application and are not available for
   six categories. */
.app-bell-entry-icon {
    flex-shrink: 0;
    margin-top: 0.3rem;
    color: var(--bs-secondary-color);
}

.app-bell-entry-body {
    min-width: 0;
}

.app-bell-entry:last-child {
    border-bottom: 0;
}

.app-bell-entry:hover,
.app-bell-entry:focus-visible {
    background-color: var(--bs-tertiary-bg);
    color: inherit;
}

/* What arrived since the last look. A bar down the side rather than a colour
   behind the text: red, amber and green carry meaning in this application
   (07-ui-ux.md) and are not available for decoration. */
.app-bell-entry-new {
    border-left: 3px solid var(--bs-primary);
    padding-left: calc(0.75rem - 3px);
}

.app-bell-entry-text {
    display: block;
    font-size: 0.85rem;
}

.app-bell-entry-meta {
    display: block;
    font-size: 0.7rem;
    color: var(--bs-secondary-color);
}

.app-bell-empty {
    padding: 0.75rem;
    font-size: 0.85rem;
    color: var(--bs-secondary-color);
}

@media (prefers-reduced-motion: reduce) {
    /* The number and the dot still say it. Somebody who has asked their system
       for less movement has asked for exactly this. */
    .app-bell-new [data-bell-toggle] {
        animation: none;
    }
}

/* The handle that folds a card away (card-fold.js). It sits in front of the
   heading and is quiet on purpose — it is furniture, not one of the buttons the
   card carries. The header becomes a flex row so the handle and whatever else
   is up there sit on one line. */
.card-header[data-fold] {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.app-fold-toggle {
    display: inline-flex;
    align-items: center;
    padding: 0;
    border: 0;
    background: none;
    color: var(--bs-secondary-color);
    line-height: 1;
}

.app-fold-toggle:hover,
.app-fold-toggle:focus-visible {
    color: var(--bs-body-color);
}

.app-fold-chevron {
    transition: transform 0.15s ease-out;
}

.app-fold-toggle.is-closed .app-fold-chevron {
    transform: rotate(-90deg);
}

@media (prefers-reduced-motion: reduce) {
    .app-fold-chevron {
        transition: none;
    }
}

/* The trail back up the tree. Sits above the page heading, so the eye finds the
   heading first and the path only when it looks for it. */
/* Table headings at the same weight as .fw-semibold, rather than the browser's
   bold. Set once here instead of a class on every th — there are fourteen
   tables, and one that forgets it would be the odd one out.

   No !important: this beats the browser's own rule on its own, and Bootstrap's
   fw-* utilities carry !important themselves, so a heading that really has to
   be heavier can still say so. */
.table th {
    font-weight: 600;
}

/* No line under the last row. Bootstrap draws one under every row including the
   final one, which then sits a hair above the card's own border — two lines
   saying the same thing, and the lower one slightly off. Every table here lives
   in a card, so the card is what closes it.
 *
 * Scoped to the last section rather than to tbody: a table with a <tfoot> keeps
 * the line under its body, because there the rule separates the rows from the
 * total. */
.table > :last-child > tr:last-child > * {
    border-bottom-width: 0;
}

/* A card of its own, decided at the mock-up (D-111). It carries the same
   border, radius and ground as the cards below it, so the page reads as a
   stack of blocks rather than a line floating over the first one. */
.app-breadcrumb {
    --bs-breadcrumb-divider: '›';
    margin-bottom: 0.75rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--bs-border-color);
    border-radius: 0.375rem;
    background-color: var(--bs-body-bg);
    font-size: 0.85rem;
}

.app-breadcrumb .breadcrumb {
    margin-bottom: 0;
    flex-wrap: wrap;
}

/* The caret that unfolds the other pages of a customer (D-111). Quiet until
   it is wanted: it sits inside a line people read, not click. */
.app-breadcrumb-more {
    margin-left: 0.15rem;
    padding: 0 0.15rem;
    color: var(--bs-secondary-color);
    text-decoration: none;
    line-height: 1;
}

.app-breadcrumb-more:hover,
.app-breadcrumb-more:focus-visible,
.app-breadcrumb-more.show {
    color: var(--bs-body-color);
}

/* Bootstrap turns the caret of an open dropdown by itself only on .dropdown-toggle,
   and that class brings its own arrow drawn in CSS. This is our sprite icon. */
.app-breadcrumb-more.show .app-icon {
    transform: rotate(180deg);
}

/* Against the item text, not against the badge: without it the number sits on
   the very edge of a narrow menu. */
.app-breadcrumb .dropdown-menu {
    --bs-dropdown-min-width: 13rem;
    font-size: 0.85rem;
}

/* Photo tiles: square thumbnails that wrap, so a check item with eight
   pictures does not push the page sideways. */
.photo-gallery {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

/* A tile is exactly as wide as its thumbnail, so the caption underneath cannot
   stretch it and push the next one onto its own row. */
.photo-tile {
    width: 120px;
}

.photo-tile img {
    display: block;
    width: 120px;
    height: 120px;
    object-fit: cover;
    border: 1px solid var(--bs-border-color);
    border-radius: 0.375rem;
    background-color: var(--bs-secondary-bg);
}

.photo-tile figcaption {
    text-align: center;
    line-height: 1.2;
    padding-top: 0.25rem;
}

.photo-tile figcaption .btn {
    font-size: 0.8rem;
}

/* A document that is not a picture. Same footprint as a thumbnail, so a wall of
   files stays a grid — it just says what it is instead of showing it. */
.document-file {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 120px;
    height: 120px;
    border: 1px solid var(--bs-border-color);
    border-radius: 0.375rem;
    background-color: var(--bs-secondary-bg);
    text-decoration: none;
    color: var(--bs-body-color);
}

.document-file-kind {
    font-weight: 600;
    letter-spacing: 0.05em;
}

.document-file-size {
    font-size: 0.8rem;
    color: var(--bs-secondary-color);
}

/* Calendar (D-085, quieter since D-086). A grid is a table — no library for
   this, and the one thing such a library is really for, dragging appointments
   about, is a piece of work of its own.

   Restrained on purpose: a month is 35 cells, and if every one of them carries
   a coloured badge the page stops having a foreground. Colour is left for the
   one thing that has to be noticed — an appointment that slipped by. */
.calendar-week,
.calendar-month {
    border-color: var(--bs-border-color);
}

/* The band of weekday names. It used to borrow the dark bar's colour; since the
   bar follows the colour mode (D-107) it takes the tinted surface instead —
   what the band is for is separating the header row from the grid, not being
   dark. */
.calendar-week thead th,
.calendar-month thead th {
    background-color: var(--bs-tertiary-bg);
    color: var(--bs-body-color);
    border-color: var(--bs-border-color);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-align: center;
    padding: 0.4rem 0.5rem;
}

.calendar-week td,
.calendar-month td {
    vertical-align: top;
    min-width: 7.5rem;
    padding: 0.4rem 0.5rem;
}

/* Room to breathe, and every row the same height — that is most of what makes
   a calendar look calm. */
.calendar-month td {
    height: 6.5rem;
}

.calendar-week td {
    height: 4.5rem;
}

.calendar-side {
    white-space: nowrap;
    background-color: var(--bs-tertiary-bg);
    font-weight: 600;
    font-size: 0.85rem;
}

/* Today is a tinted cell, not a ring. A two-pixel frame around one square of
   thirty-five shouts. */
.calendar-week .is-today,
.calendar-month .is-today {
    background-color: var(--app-tint);
}

.calendar-month .is-today .calendar-day-number {
    color: var(--bs-body-color);
    font-weight: 700;
}

/* Away, and simply not a working day — for dispatching the two mean the same
   thing but not for the same reason, so they look different. Both stay pale:
   they are the background of the week, not its message. */
.calendar-week .is-absent {
    background-color: var(--bs-tertiary-bg);
}

.calendar-week .is-off {
    background-image: repeating-linear-gradient(
        45deg,
        transparent,
        transparent 5px,
        var(--bs-tertiary-bg) 5px,
        var(--bs-tertiary-bg) 10px
    );
}

.calendar-unassigned td {
    background-color: var(--app-tint-warm);
}

.calendar-unassigned .calendar-side {
    color: var(--bs-secondary-color);
}

.calendar-note {
    display: block;
    font-size: 0.7rem;
    color: var(--bs-secondary-color);
}

/* An entry is a line of text, not a chip. Borders on every appointment turn a
   week into a lattice. */
.calendar-entry {
    display: block;
    padding: 0.1rem 0 0.1rem 0.4rem;
    border-left: 2px solid var(--bs-border-color);
    font-size: 0.78rem;
    line-height: 1.3;
    text-decoration: none;
    color: var(--bs-body-color);
}

.calendar-entry + .calendar-entry {
    margin-top: 0.15rem;
}

.calendar-entry:hover {
    border-left-color: var(--bs-link-color);
    background-color: var(--app-tint);
}

/* The appointment that slipped by. The only colour left in the grid, and it
   keeps it for exactly that reason. */
.calendar-entry.is-overdue {
    border-left-color: var(--bs-danger);
    color: var(--bs-danger-text-emphasis);
}

.calendar-entry-time {
    color: var(--bs-secondary-color);
    margin-right: 0.3rem;
    font-variant-numeric: tabular-nums;
}

.calendar-entry-label {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;
}

.calendar-month .is-outside {
    background-color: var(--bs-secondary-bg);
}

.calendar-month .is-outside .calendar-day-number {
    color: var(--bs-secondary-color);
}

.calendar-day-number {
    display: block;
    margin-bottom: 0.2rem;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--bs-secondary-color);
    text-decoration: none;
}

.calendar-day-number:hover {
    color: var(--bs-body-color);
}

.calendar-count {
    display: block;
    font-size: 0.78rem;
    line-height: 1.3;
}

.calendar-away {
    display: block;
    font-size: 0.7rem;
    color: var(--bs-secondary-color);
}

/* A PDF whose first page was rendered as a preview. It looks like a picture, so
   it has to say that it is not one — a click fetches the document, not a
   larger image. */
.document-preview {
    position: relative;
    display: block;
    width: 120px;
}

.document-preview .document-kind {
    position: absolute;
    right: 0.25rem;
    bottom: 0.25rem;
    padding: 0 0.25rem;
    border-radius: 0.2rem;
    background-color: rgba(0, 0, 0, 0.7);
    color: #fff;
    font-size: 0.7rem;
    letter-spacing: 0.05em;
}

/* A page is portrait; showing it cropped to a square would cut the top off. */
.document-preview img {
    object-fit: contain;
    background-color: var(--bs-body-bg);
}

/* Viewer: the picture fills the dialog, zooming happens inside it. */
.photo-viewer .modal-body {
    overflow: hidden;
    background-color: #1d2327;
    /* Lets a phone pinch-zoom without the page moving underneath. */
    touch-action: pinch-zoom;
}

.photo-viewer img {
    display: block;
    margin: 0 auto;
    max-width: 100%;
    max-height: 75vh;
    transform-origin: center center;
    transition: transform 0.15s ease-out;
    cursor: zoom-in;
}

.photo-viewer img.is-zoomed {
    cursor: grab;
    transition: none;
    /* While zoomed, one finger moves the picture instead of scrolling the
       dialog. Unzoomed the browser keeps its pinch gesture. */
    touch-action: none;
    user-select: none;
    -webkit-user-drag: none;
}

.photo-viewer img.is-dragging {
    cursor: grabbing;
}

/* Timeline: a plain vertical line with a dot per entry. The history is meant to
   be scanned, not admired. */
.timeline {
    position: relative;
    padding-left: 1.25rem;
}

.timeline::before {
    content: '';
    position: absolute;
    left: 0.3rem;
    top: 0.35rem;
    bottom: 0.35rem;
    width: 1px;
    background-color: var(--bs-border-color);
}

.timeline-entry {
    position: relative;
    padding-bottom: 1rem;
}

.timeline-entry:last-child {
    padding-bottom: 0;
}

.timeline-entry::before {
    content: '';
    position: absolute;
    left: -1.1rem;
    top: 0.45rem;
    width: 0.55rem;
    height: 0.55rem;
    border-radius: 50%;
    background-color: var(--bs-secondary-color);
}

/*
 * Wide tables turn into one card per row on narrow screens (07-ui-ux.md).
 * Sideways scrolling is deliberately NOT the fallback — it is unusable on a
 * phone. The markup carries the column caption in data-label.
 *
 * The footer stacks with the body (D-180). It used to be left out, so a summing
 * row stayed four columns wide and printed its figures with no captions at all:
 * "Summe 150,00 110,00 40,00", three numbers nobody could tell apart. It is the
 * one row where that hurts most — the totals are what somebody scrolls down for.
 *
 * A cell holds exactly two flex items here: the caption from ::before and the
 * content. Anything that puts several nodes in a cell has to wrap them in one
 * element, or space-between pulls them apart across the whole width.
 */
@media (max-width: 767.98px) {
    .table-stack thead {
        display: none;
    }

    .table-stack tbody tr,
    .table-stack tfoot tr {
        display: block;
        margin-bottom: 0.75rem;
        border: 1px solid var(--bs-border-color);
        border-radius: 0.375rem;
        background-color: var(--bs-body-bg);
    }

    .table-stack tbody td,
    .table-stack tfoot td {
        display: flex;
        justify-content: space-between;
        gap: 1rem;
        border: 0;
        border-bottom: 1px solid var(--bs-border-color);
    }

    .table-stack tbody tr td:last-child,
    .table-stack tfoot tr td:last-child {
        border-bottom: 0;
    }

    .table-stack tbody td::before,
    .table-stack tfoot td::before {
        content: attr(data-label);
        font-weight: 600;
        color: var(--bs-secondary-color);
    }

    /* The first cell of a summing row names the row, it does not hold a value:
       stacked with its column caption it read "Position: Summe". Centred and
       without the caption it is what it is — the heading of the block under it. */
    .table-stack tfoot td:first-child {
        justify-content: center;
    }

    .table-stack tfoot td:first-child::before {
        content: none;
    }
}

/* A column that should take no more room than its contents — a checkbox, a
   single icon. One per cent is the usual way of saying "as narrow as you can":
   the browser gives it the minimum it needs and hands the rest to the others.
   Only above the stacking breakpoint; stacked, every cell is a row of its own. */
@media (min-width: 768px) {
    .col-tight {
        width: 1%;
        white-space: nowrap;
    }
}

/* The analysis chart (D-132, D-134).
 *
 * The colours live here rather than in the JavaScript: the module reads them
 * off this element, so light and dark are decided in one place and Chart.js
 * only draws what it is handed.
 *
 * They are the first two categorical slots of a validated palette, checked
 * against the surfaces they actually sit on — lightness band, chroma,
 * colour-blind separation and 3:1 contrast pass in both modes. Deliberately not
 * the Bootstrap theme colours: red, amber and green carry meaning in this
 * system (defect, request, released), and a chart must not borrow them. The
 * dark steps are chosen for the dark surface, not flipped from the light ones. */
.analysis-chart {
    --chart-revenue: #2a78d6;
    --chart-cost: #eb6834;
    --chart-grid: #e1e0d9;
    --chart-ink: #52514e;

    /* Height belongs to the container, not to the canvas: Chart.js resizes to
       whatever it is given, and a canvas without a box grows without end. */
    position: relative;
    height: 20rem;
}

[data-bs-theme="dark"] .analysis-chart {
    --chart-revenue: #3987e5;
    --chart-cost: #d95926;
    --chart-grid: #2c2c2a;
    --chart-ink: #c3c2b7;
}

/* The permission matrix (D-144).
 *
 * Fifty rows and eight columns: whoever scrolls to the bottom has long lost
 * which column is which, and whoever scrolls sideways has lost which row. Both
 * headers therefore stay put inside the box rather than the page scrolling —
 * the work area already scrolls, and a table that pushes it further makes the
 * two fight.
 *
 * position: sticky on the cells, not on thead: a table header row is not a
 * containing block, and sticking the row itself does nothing in any browser. */
.permission-matrix {
    max-height: min(70vh, 44rem);
    overflow: auto;
}

.permission-matrix thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    /* Its own background, or the rows would show through as they pass under. */
    background-color: var(--bs-body-bg);
}

/* The row label goes with it, for the same reason in the other direction. */
.permission-matrix tbody th {
    position: sticky;
    left: 0;
    z-index: 1;
    background-color: var(--bs-body-bg);
}

.permission-matrix thead th:first-child {
    left: 0;
    z-index: 3;
}

/* The group heading spans the whole width, so it needs the tinted background
   back — the rule above would otherwise paint it like an ordinary row. */
.permission-matrix tbody th[colspan] {
    background-color: var(--bs-tertiary-bg);
}

/* What has been changed and not yet saved.
 *
 * Red for the newly ticked one, and it took Arash trying it to see why: a
 * ticked Bootstrap checkbox is itself blue, so a blue outline around it
 * disappears into the very thing it is meant to point at. Red is the one colour
 * that cannot be confused with a tick.
 *
 * It does mean red carries a second meaning on this page — everywhere else it
 * is a defect (07-ui-ux.md). Bearable here because nothing on this page is
 * graded, and because both states say it in words as well for anybody who does
 * not read colour. */
.permission-cell.is-added {
    outline: 2px solid var(--chart-added, #c02b2b);
    outline-offset: 2px;
    border-radius: .25rem;
}

.permission-cell.is-removed {
    outline: 2px dashed var(--chart-removed, #b06a00);
    outline-offset: 2px;
    border-radius: .25rem;
}

[data-bs-theme="dark"] .permission-cell.is-added {
    --chart-added: #e5564f;
}

[data-bs-theme="dark"] .permission-cell.is-removed {
    --chart-removed: #d99a2b;
}

/* Ticked, and doing nothing, because what it depends on is not
   (Permission::requires). Marked rather than blocked: granting it is not wrong,
   it is half done, and root may be one click away from the other half.

   On the cell rather than on the box, because the box's outline already means
   "changed and not saved yet" — two meanings on one mark is how a signal stops
   being one. Kept to a faint ground and a tooltip: it is a remark, not a
   warning, and the row underneath the name says the rule in words for anybody
   who does not read colour or does not hover.

   Written with the container and the element in front of the class on purpose.
   Bootstrap paints every cell from `.table > :not(caption) > * > *`, which
   outweighs a lone class — the mark would have been in the stylesheet and
   invisible on screen. box-shadow is left alone for the same reason: that is
   what Bootstrap tints rows with, and taking it over would drop the hover. */
.permission-matrix td.permission-cell-inert {
    /* --bs-secondary-bg, not --bs-tertiary-bg: measured in the browser, the
       tertiary one sits seven points off white and is a mark nobody would
       notice. This one is still quiet and actually visible. Both follow the
       colour mode, so the dark theme needs no rule of its own. */
    background-color: var(--bs-secondary-bg);
}

/* The count of unsaved changes, so the outlines are not the only signal. */
.permission-changes:empty {
    display: none;
}

/* The logo of a service provider. A box it fits inside rather than a size it is
   forced into: logos are wide, tall and square, and object-fit: contain keeps
   every one of them undistorted and on the same line as the next. */
.provider-logo {
    display: block;
    max-width: 100%;
    width: auto;
    height: auto;
    max-height: 120px;
    object-fit: contain;
}

/* A sortable column heading (D-166). It has to read as a heading first and as a
   link second — blue underlined text across the top of every table would look
   like eight things to click before it looks like a table. */
.app-sort {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    color: inherit;
    text-decoration: none;
}

.app-sort:hover,
.app-sort:focus-visible {
    color: var(--bs-link-color);
}

/* Quiet until it means something: the double chevron only says "this one can be
   sorted", while the filled arrow says what the list is actually doing. Same
   size for all three, or the heading would shift a pixel on every click. */
.app-sort-icon {
    opacity: 0.4;
    flex: none;
}

.app-sort:hover .app-sort-icon,
.app-sort-active .app-sort-icon {
    opacity: 1;
}

/* The basket under the order form. It sticks to the foot of its column, so the
   list inside it has to stay short enough to leave the picker visible — a
   basket that fills the screen is worse than one that scrolls away. Its own
   scrollbar rather than a cap on how many rows are shown: forty units are a
   real order, and "and 32 more" is exactly the sentence somebody would have to
   scroll to check.

   Less of it on a phone, measured rather than guessed: seven lines took 45 % of
   a 780-pixel screen and left the picker a hand's width to work in. At a
   quarter the usual order — three or four lines — still stands unscrolled, and
   the big one keeps its own scrollbar. Whoever wants the room back folds the
   whole thing away, and it stays folded. */
.app-basket-body {
    max-height: 25vh;
    overflow-y: auto;
}

@media (min-width: 768px) {
    .app-basket-body {
        max-height: 45vh;
    }
}

/* Bootstrap's close button, taken down to the size of the line it sits in — it
   is drawn in em, so the font size is the whole handle. */
.app-basket-remove {
    font-size: 0.55rem;
    flex: none;
}
