﻿html, body {
    height: 100%;
    margin: 0;
    font-family: var(--font-sans);
    font-size: 14px;
    background-color: var(--background);
    color: var(--foreground);
    color-scheme: light;
}

.dark body,
html.dark {
    color-scheme: dark;
}

/* The app shell owns all scrolling (sidebar and content are the only scroll
   containers). Without this, any stray flow content makes the document itself
   scrollable, and wheel input over the nav moves the whole page. Guarded by
   :has so standalone documents (login page) keep normal document scrolling. */
body:has(.shell) {
    overflow: hidden;
}

*, *::before, *::after {
    box-sizing: border-box;
}

a {
    color: inherit;
}

h1:focus {
    outline: none;
}

::selection {
    background-color: var(--primary);
    color: var(--primary-foreground);
}

/* Slim scrollbars matching the dark design */
* {
    scrollbar-width: thin;
    scrollbar-color: var(--border) transparent;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid var(--alert-success);
}

.invalid {
    outline: 1px solid var(--destructive);
}

.validation-message {
    color: var(--destructive);
}

.blazor-error-boundary {
    background-color: var(--alert-danger-bg);
    border: 1px solid var(--alert-danger);
    border-radius: var(--radius);
    padding: 1rem;
    color: var(--alert-danger-foreground);
}

    .blazor-error-boundary::after {
        content: "Ein Fehler ist aufgetreten.";
    }

/* Shared layout for the administration pages (clients, projects, shootings).

   The content area already has a definite height — it is the 1fr row of a shell
   that is 100vh tall — but it never passed that on, so every page ended where
   its content ended and the rest of a large screen stayed empty. This is where
   it is passed on: height rather than min-height, because a list can only end
   at the bottom edge of a box that has a bottom edge. */
.admin-page {
    display: flex;
    flex-direction: column;
    height: 100%;
}

/* Nothing here shrinks below what it says — only the part that asked to fill
   grows. Without this the fixed height above would squeeze a page's cards and
   filters instead of letting them run past it, with .content scrolling. */
.admin-page > :not(.grid-fill) {
    flex: none;
}

.admin-page h1 {
    margin: 0;
    font-size: 1.35rem;
    font-weight: 700;
    letter-spacing: -0.01em;
}

.page-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}

.form-field {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    margin-bottom: 1rem;
}

.form-field-inline {
    flex-direction: row;
    align-items: center;
    gap: 0.5rem;
}

.form-error {
    margin: 0;
    font-size: 0.8rem;
    color: var(--destructive);
}

.form-hint {
    margin: 0;
    font-size: 0.8rem;
    color: var(--muted-foreground);
}

/* The further library folders on the settings page: one per line with its
   remove button, and the field to add one below them. */
.root-list {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    margin: 0 0 0.5rem;
    padding: 0;
    list-style: none;
}

.root-list li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
}

.root-path {
    overflow-wrap: anywhere;
    font-size: 0.85rem;
}

.root-add {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* The copy directory on the shooting's detail page: one place per line, its count
   beside the name rather than under it — the list is read down the names, and a
   two-line entry per place would make four places look like eight. */
.copy-places {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.copy-place {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
}

/* The small "i" of HelpTip, where one explanation covers a whole card or a row of
   icon buttons. Its wrapper is display:contents, so colour reaches the icon by
   inheritance and no box appears in the surrounding flex row. */
.help-tip {
    color: var(--muted-foreground);
    cursor: help;
}

/* The design system's reset makes every svg display:block, which would drop the
   icon onto a line of its own behind a heading. */
.help-tip svg {
    display: inline-block;
    vertical-align: -0.15em;
}

/* Something did not go to plan but nothing is broken and nobody has to act —
   the automatic rollback of a server update is the case this exists for. */
.form-warning {
    margin: 0;
    font-size: 0.8rem;
    color: var(--alert-warning-foreground);
}

.row-actions {
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

.row-actions .icon-button {
    display: grid;
    place-items: center;
    width: 1.9rem;
    height: 1.9rem;
    border: none;
    border-radius: 0.5rem;
    background: transparent;
    color: var(--muted-foreground);
    cursor: pointer;
}

.row-actions .icon-button:hover {
    background: var(--muted);
    color: var(--foreground);
}

.grid-empty {
    padding: 2rem 1rem;
    text-align: center;
    font-size: 0.85rem;
    color: var(--muted-foreground);
}

/* A data grid that takes the rest of the page: the table scrolls inside it and
   the pagination row stays at the bottom edge of the window. Both classes go to
   BbDataGrid — Class lands on its root element, TableContainerClass on the
   scrolling wrapper around the <table>, and the pagination row is that
   wrapper's sibling, so a flex column puts it last. */
.grid-fill {
    display: flex;
    flex-direction: column;
    /* Basis zero, so the space it gets is what is left rather than what its rows
       would need — a hundred rows must not push the footer off the screen. */
    flex: 1 1 0;
    /* Under this a frame is not worth filling any more, and a page with tall
       cards above its list would otherwise squeeze the list to nothing. */
    min-height: 16rem;
}

.grid-fill-table {
    flex: 1 1 0;
    /* Without this the scroll container refuses to shrink below its content and
       the page grows a second scrollbar instead of the table getting its own. */
    min-height: 0;
}

/* Card width on the form pages. The design system ships max-w-sm/md/lg/2xl but
   no max-w-xl, and the app has no Tailwind build of its own — the 36 places
   asking for it were styling nothing, so every form card ran the full width of
   the window. 36rem is the step the missing utility stands for. */
.max-w-xl {
    max-width: 36rem;
}

/* Settings: the list of sections beside the open one (SettingsLayout). Measured
   against the page, not the window — the app's own sidebar takes 15rem of it,
   so a window wide enough for two columns can still leave too little room for
   a card beside the list. Below that the list becomes a row above the section. */
.settings-page {
    container-type: inline-size;
}

.settings-shell {
    display: grid;
    grid-template-columns: 11rem minmax(0, 1fr);
    gap: 2.5rem;
    align-items: start;
    margin-top: 1.5rem;
}

/* Stays in view while a long section scrolls under it; five entries fit any
   window this runs in. */
.settings-nav {
    position: sticky;
    top: 0;
    display: flex;
    flex-direction: column;
    gap: 0.125rem;
}

/* Quieter than the main navigation on purpose: the app's sidebar already marks
   "Settings" in the accent colour, and a second amber entry beside it would
   compete for the same meaning. */
.settings-nav-link {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.45rem 0.75rem;
    border-radius: var(--radius);
    font-size: 0.85rem;
    color: var(--muted-foreground);
    text-decoration: none;
    white-space: nowrap;
    transition: background-color 0.15s ease, color 0.15s ease;
}

.settings-nav-link:hover {
    background-color: var(--accent);
    color: var(--accent-foreground);
}

.settings-nav-link:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 2px;
}

.settings-nav-link.active {
    background-color: var(--accent);
    color: var(--foreground);
    font-weight: 600;
}

.settings-body {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
    min-width: 0;
}

.settings-section-header {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    max-width: 36rem;
}

/* The layout's "Settings" wears the page-heading look without being the h1. */
.settings-title {
    margin: 0;
    font-size: 1.35rem;
    font-weight: 700;
    letter-spacing: -0.01em;
}

.settings-section-header h1 {
    margin: 0;
    font-size: 1.2rem;
    font-weight: 700;
    letter-spacing: -0.01em;
}

/* Page, section, card: three levels here where other pages have two, so the
   card titles step down from the design system's 1.5rem — otherwise every card
   would outrank the section it belongs to. */
.settings-body .max-w-xl h3 {
    font-size: 1.05rem;
    line-height: 1.3;
}

/* A card's introductory hint stood flush against the first field label under
   it, and read as that field's hint. */
.settings-body .max-w-xl .form-hint + .form-field {
    margin-top: 0.75rem;
}

/* A card title with the device mark beside it rather than under it. */
.card-title-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem 0.75rem;
}

@container (max-width: 44rem) {
    .settings-shell {
        grid-template-columns: minmax(0, 1fr);
        gap: 1.25rem;
    }

    /* A row that scrolls sideways rather than wrapping: five entries on two
       lines read as two groups. */
    .settings-nav {
        position: static;
        flex-direction: row;
        overflow-x: auto;
        scrollbar-width: none;
        /* A scroll container clips at its padding box, and the focus ring sits
           4px outside a link — without room on every side it lost its edges. */
        padding: 4px 4px 0.5rem;
        border-bottom: 1px solid var(--border);
    }

    .settings-nav-link {
        flex: none;
    }
}

/* "This device only" on a card or beside a field (DeviceScopeBadge). Muted: it
   is a fact about where the value is kept, not a warning. */
.scope-badge {
    font-weight: 500;
    color: var(--muted-foreground);
    white-space: nowrap;
}

/* A label whose text carries a mark beside it, above the control it names. */
.field-label-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* Filter bar above a data grid (shootings, assets). Wraps on narrow widths. */
.filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

.filters > * {
    min-width: 10rem;
}

/* The offer to analyse a shooting, sitting in the Assets filter row. Its three
   classes were never defined, so it rendered as three unstyled blocks between
   the filters; this makes it one line — shooting on the left, buttons right. */
.analysis-offer {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.3rem 0.3rem 0.3rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: 0.5rem;
    background: var(--card);
    font-size: 0.85rem;
}

.analysis-offer-text {
    display: flex;
    align-items: baseline;
    gap: 0.4rem;
    min-width: 0;
}

.analysis-offer-actions {
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

/* Capture-metadata range and facet filters below the main filter bar (asset catalog). */
.filters-metadata {
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem;
}

.filter-range {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

.filter-range-label {
    font-size: 0.85rem;
    color: var(--muted-foreground);
}

.filter-range-input {
    width: 4.5rem;
    padding: 0.25rem 0.5rem;
    border: 1px solid var(--border);
    border-radius: 0.4rem;
    background: var(--background);
    color: inherit;
}

.filters-facets {
    display: flex;
    flex-wrap: wrap;
    gap: 2rem;
}

.filter-facet {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    max-height: 8rem;
    overflow-y: auto;
}

.facet-label {
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--muted-foreground);
}

.facet-option {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.85rem;
}

/* Text that only assistive technology reads: the names of the dropdowns in
   the toolbar, whose visible placeholder is a value and not a label. */
.visually-hidden,
.visually-hidden-focusable:not(:focus) {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.visually-hidden-focusable:focus {
    position: absolute;
    z-index: 20;
    padding: 0.5rem 0.75rem;
    background: var(--background);
    border: 1px solid var(--primary);
    border-radius: 0.375rem;
}

/* The asset browser's toolbar: one row, stuck to the top of the scrolling
   content while the grid goes past underneath. The generic .filters rule above
   gives every child a minimum width and lets a block-level dropdown take the
   whole row; here each control gets the width its content deserves instead. */
.assets-toolbar {
    position: sticky;
    top: 0;
    z-index: 5;
    margin-top: 1rem;
    padding: 0.75rem 0;
    background: var(--background);
    border-bottom: 1px solid var(--border);
}

.assets-toolbar .filters > * {
    min-width: 0;
}

.assets-toolbar .filter-search {
    flex: 1 1 14rem;
    max-width: 22rem;
}

.filter-field {
    display: block;
    flex: 0 1 auto;
    width: 12rem;
}

.filter-field-wide {
    width: 17rem;
}

.filter-field-narrow {
    width: 8rem;
}

.filter-field-sort {
    width: 11rem;
}

.filter-segment {
    flex: 0 0 auto;
}

/* Sorting and the fold live at the end of the row, together, so the eye
   finds "what order" and "what else" in one place. */
.filter-toolbar-end {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-left: auto;
}

/* The chips: what is narrowing the grid, each removable where it is read.
   A list, so a screen reader announces how many there are. */
.filter-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    margin: 0.5rem 0 0;
    padding: 0;
    list-style: none;
}

.filter-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.125rem 0.25rem 0.125rem 0.6rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--muted);
    font-size: 0.8rem;
    line-height: 1.4;
}

.filter-chip button {
    display: grid;
    place-items: center;
    width: 1.25rem;
    height: 1.25rem;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--muted-foreground);
    cursor: pointer;
}

.filter-chip button:hover {
    background: var(--accent);
    color: var(--foreground);
}

.filter-chip button:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 1px;
}

/* The folded-away filters, in three columns that wrap to one on a narrow
   window. A panel and not a card: it belongs to the toolbar above it. */
.filter-panel {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
    gap: 1.25rem 2rem;
    margin-top: 0.75rem;
    padding: 1rem;
    border: 1px solid var(--border);
    border-radius: 0.5rem;
    background: var(--card);
}

.filter-panel[hidden] {
    display: none;
}

.filter-group {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.6rem;
}

.filter-group-title {
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--muted-foreground);
}

/* Within the panel the facet lists sit side by side, not stacked. */
.filter-panel .filters-facets {
    flex-direction: row;
    flex-wrap: wrap;
    gap: 1.25rem 2rem;
}

.filter-panel .filters-facets .filter-group-title {
    width: 100%;
}

/* One persistent row between toolbar and grid: it offers the pick, or it
   carries the verbs — so the grid never moves when a selection starts. */
.selection-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    min-height: 2.75rem;
    margin-top: 0.75rem;
    padding: 0.25rem 0.5rem;
    border: 1px solid transparent;
    border-radius: 0.5rem;
}

.selection-bar-active {
    border-color: var(--border);
    background: var(--muted);
}

.selection-count {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    margin-right: 0.5rem;
    font-size: 0.85rem;
    font-weight: 600;
}

.selection-move {
    display: flex;
    align-items: center;
    gap: 0.375rem;
}

.selection-bar-end {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    margin-left: auto;
}

.selection-more {
    width: 100%;
    padding-top: 0.5rem;
    border-top: 1px solid var(--border);
}

.selection-more[hidden] {
    display: none;
}

/* The selection bar's second row: a keyword to add or remove, and a colour
   label for everything picked. Wraps rather than squeezing, so a narrow window
   costs a line instead of legibility. */
.bulk-metadata {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

.bulk-metadata-field {
    flex: none;
    width: 14rem;
}

/* The colour of a colour label is the label. Bordered so a yellow one is still
   there on a light background. */
.color-label-swatch {
    display: inline-block;
    flex: none;
    width: 0.7rem;
    height: 0.7rem;
    border: 1px solid var(--border);
    border-radius: 50%;
}

.grid-link {
    color: var(--foreground);
    font-weight: 500;
    text-decoration: none;
}

.grid-link:hover {
    text-decoration: underline;
}

/* Shooting detail page. */
.detail-page {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
}

.detail-back {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    font-size: 0.85rem;
    color: var(--muted-foreground);
    text-decoration: none;
}

.detail-back:hover {
    color: var(--foreground);
}

.detail-grid {
    display: grid;
    grid-template-columns: minmax(8rem, max-content) 1fr;
    gap: 0.6rem 1.5rem;
    font-size: 0.9rem;
}

.detail-grid dt {
    color: var(--muted-foreground);
}

.detail-grid dd {
    margin: 0;
    color: var(--foreground);
    /* A description is prose, and prose stops being readable somewhere around
       here — without the cap it runs the full width of a large screen. */
    max-width: 80ch;
}

/* Compact link lists on detail pages (a client's projects/shootings, …). */
.detail-list {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    font-size: 0.9rem;
}

.detail-list li {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

/* Inbox preview next to an upload's file name. Without a size the browser takes
   the thumbnail's own 512px edge and one row fills the window. */
.inbox-thumb {
    flex: none;
    width: 2.5rem;
    height: 2.5rem;
    object-fit: cover;
    border-radius: 0.35rem;
    background: var(--muted);
}

/* Card header with a trailing badge (e.g. asset count). */
.detail-card-header {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}

.editing-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

/* Block-level loading placeholder for slow initial data loads. */
.loading-indicator {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 2.5rem 0;
    color: var(--muted-foreground);
    font-size: 0.9rem;
}

/* Where the whole page is still loading it stands in the middle of it, not in
   the top left corner of an otherwise empty screen. Inside a card it keeps the
   plain left-aligned form. */
.admin-page > .loading-indicator {
    flex: 1;
    justify-content: center;
}

.loading-spinner {
    width: 1.1rem;
    height: 1.1rem;
    border: 2px solid var(--border);
    border-top-color: var(--primary);
    border-radius: 50%;
    animation: loading-spin 0.7s linear infinite;
}

@keyframes loading-spin {
    to {
        transform: rotate(360deg);
    }
}

/* Asset catalog thumbnail grid. */
.asset-grid {
    display: grid;
    /* The tile width comes from the gallery's size control as an inline custom
       property; the fallback is the fixed width the grid had before it was
       adjustable, so a grid rendered without the bar looks unchanged.

       min(..., 100%) because the largest step is 24rem and a phone is narrower
       than that: without it the column keeps its minimum and the page scrolls
       sideways. Deliberately not a media query — the same thing happens in a
       narrow window on a desktop. */
    grid-template-columns: repeat(auto-fill, minmax(min(var(--asset-tile-width, 11rem), 100%), 1fr));
    gap: 1rem;
    margin-top: 1.25rem;
}

/* The two spacers <Virtualize> keeps around the mounted batches are grid items
   now, and a grid item takes a cell: left alone they each sit where a tile
   belongs and push the row out of alignment. */
.asset-grid > div:not(.asset-tile) {
    grid-column: 1 / -1;
}

.asset-tile {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--border);
    border-radius: 0.6rem;
    overflow: hidden;
    background: var(--card);
}

.asset-tile:hover {
    border-color: var(--muted-foreground);
}

/* The thumbnail itself is the button that opens the fullscreen viewer — the
   tile as a whole isn't, so the Keeper/Reject buttons in .asset-meta below
   aren't nested inside it (invalid HTML, breaks click handling). */
.asset-thumb-button {
    display: block;
    width: 100%;
    padding: 0;
    border: none;
    background: none;
    font: inherit;
    color: inherit;
    cursor: pointer;
}

.asset-thumb-button:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
}

.asset-thumb {
    display: grid;
    place-items: center;
    aspect-ratio: 4 / 3;
    background: var(--muted);
    color: var(--muted-foreground);
    overflow: hidden;
}

.asset-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* The "no picture from this device" state of a tile — and, reusing the same
   class, the lightbox's "Vorschau folgt" placeholder for a RAW without a look
   (Baustein 5). Icon plus one short line, because the icon on its own is what
   the tile also shows while it waits — and that ambiguity is the whole reason
   this state exists. Muted rather than red: nothing is broken, this machine
   simply produced no preview yet. The text wraps inside the thumbnail box,
   which keeps the smallest tile step readable instead of pushing the grid
   wider. Global rather than scoped to one component: AssetGallery and
   AssetLightbox both render it, and Blazor's CSS isolation would otherwise
   scope the rule to whichever component's .razor.css defined it. */
.asset-thumb-unavailable {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    max-width: 100%;
    padding: 0.4rem;
    overflow: hidden;
    color: var(--muted-foreground);
    font-size: 0.7rem;
    line-height: 1.2;
    text-align: center;
}

.asset-meta {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    padding: 0.6rem 0.7rem;
}

.asset-name {
    font-size: 0.82rem;
    font-weight: 500;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.asset-sub {
    font-size: 0.75rem;
    color: var(--muted-foreground);
}

.asset-badges {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
}

/* Sits in the badge row rather than over the thumbnail: the tile's corner
   already belongs to the multi-select toggle, and a second overlay there would
   cover the picture the grid exists to show. Muted like the badges next to it
   so a rated tile does not shout across the grid. Also used, unchanged, by the
   lightbox's rating stars — global rather than scoped, for the same reason as
   .asset-thumb-unavailable above. */
.asset-rating {
    display: inline-flex;
    align-items: center;
    color: var(--muted-foreground);
}

/* Lightroom's flag on a tile: shown, not a control (docs/produktbild.md). */
.asset-flag {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.9rem;
    height: 1.9rem;
    margin-top: 0.15rem;
    border: 1px solid var(--border);
    border-radius: 0.4rem;
}

.asset-flag-picked {
    border-color: var(--primary);
    background: var(--primary);
    color: var(--primary-foreground);
}

.asset-flag-rejected {
    border-color: var(--destructive);
    background: var(--destructive);
    color: var(--destructive-foreground);
}

/* Fullscreen asset viewer (lightbox). */
.lightbox {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: grid;
    grid-template-columns: auto 1fr auto;
    grid-template-rows: auto 1fr;
    background: rgba(0, 0, 0, 0.92);
    color: #e5e5e5;
    outline: none;
}

.lightbox-toolbar {
    grid-column: 1 / -1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.6rem 1rem;
    background: rgba(0, 0, 0, 0.55);
}

.lightbox-info {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
    min-width: 0;
}

.lightbox-title {
    font-size: 0.9rem;
    font-weight: 500;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.lightbox-counter,
.lightbox-zoom {
    font-size: 0.8rem;
    color: #a3a3a3;
    font-variant-numeric: tabular-nums;
}

.lightbox-zoom {
    min-width: 3.2rem;
    text-align: center;
}

/* Why the original is not here, next to the file name it belongs to. Muted like
   the counter, because three of its four states are places rather than faults;
   only the one that means nobody can account for the file takes a colour, and
   it takes the same one the rest of the application uses for destructive. */
.lightbox-origin {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-width: 0;
    font-size: 0.8rem;
    color: #a3a3a3;
}

.lightbox-origin-alarming {
    color: var(--destructive);
}

.lightbox-actions {
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

.lightbox-button,
.lightbox-nav {
    display: grid;
    place-items: center;
    border: none;
    background: transparent;
    color: #d4d4d4;
    cursor: pointer;
    border-radius: 0.5rem;
}

.lightbox-button {
    width: 2.1rem;
    height: 2.1rem;
}

/* A disabled toolbar button has to look disabled, or the reveal action reads as
   broken rather than unavailable — an archived asset lives on a drive that may
   not be plugged in, and the tooltip says so. .lightbox-nav already had this. */
.lightbox-button:disabled {
    opacity: 0.35;
    cursor: default;
}

.lightbox-button:hover:not(:disabled),
.lightbox-nav:hover:not(:disabled) {
    background: rgba(255, 255, 255, 0.12);
    color: #ffffff;
}

.lightbox-button-active {
    background: rgba(255, 255, 255, 0.18);
    color: #ffffff;
}

.lightbox-nav {
    width: 3rem;
    margin: 0.5rem 0.25rem;
    align-self: stretch;
}

.lightbox-nav:disabled {
    opacity: 0.25;
    cursor: default;
}

.lightbox-stage {
    display: grid;
    place-items: center;
    overflow: hidden;
    /* Wheel zoom replaces scrolling; block browser touch panning too. */
    touch-action: none;
}

.lightbox-image {
    grid-area: 1 / 1;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    user-select: none;
    will-change: transform;
    cursor: zoom-in;
}

.lightbox-image-loading {
    visibility: hidden;
}

/* Sound in the viewer: the same grid cell the picture would occupy, so the
   toolbar, the neighbours and the metadata panel keep their places. */
.lightbox-audio {
    grid-area: 1 / 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    color: var(--muted-foreground, #94a3b8);
    user-select: none;
}

.lightbox-image-dragging {
    cursor: grabbing;
}

/* Detection overlay. Spans the whole stage rather than the picture; its viewBox
   is the asset's pixel size and "meet" fits that inside, which lands on exactly
   the rectangle the image's object-fit: contain produces. Both are centred in
   the same grid cell, so the shared pan/zoom transform moves them together. */
.lightbox-detections {
    width: 100%;
    height: 100%;
    /* Only .lightbox-image's max-* and object-fit would apply here, and neither
       means anything for an SVG that carries its own viewBox. */
    object-fit: fill;
    pointer-events: none;
}

.lightbox-detections rect {
    fill: none;
    stroke: var(--primary, #38bdf8);
    /* Scales with the viewBox, so the outline stays a hairline at any zoom
       instead of swelling into a slab. */
    stroke-width: 0.3%;
}

.lightbox-detections text {
    fill: var(--primary, #38bdf8);
    paint-order: stroke;
    /* A dark outline behind the glyphs: the label sits on the picture, and
       whatever colour that picture is, the word has to stay readable. */
    stroke: rgb(0 0 0 / 70%);
    stroke-width: 0.4%;
}

/* Focus sentinels: absolute so they stay out of the overlay's grid flow, and
   zero-sized so nothing shows even while focus passes through them. */
.lightbox-sentinel {
    position: absolute;
    width: 0;
    height: 0;
    outline: none;
}

/* Keyboard legend, toggled with "?" — absolute so it overlays the stage
   instead of taking a cell in the overlay's grid. */
.lightbox-shortcuts {
    position: absolute;
    top: 3.4rem;
    right: 1rem;
    z-index: 1;
    max-width: min(22rem, calc(100vw - 2rem));
    padding: 0.85rem 1rem;
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 0.6rem;
    background: rgba(0, 0, 0, 0.85);
}

/* Same anchor and the same dark card as the shortcut legend below — the two are
   never open at the same spot for long, and one look for both panels means one
   place to change when the toolbar moves. Wider: keywords are words. */
.lightbox-metadata {
    position: absolute;
    top: 3.4rem;
    right: 1rem;
    z-index: 1;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    max-width: min(30rem, calc(100vw - 2rem));
    /* Capture data, swatches and a wrapped row of keywords together can outgrow
       a short window; the panel scrolls rather than running off the bottom
       edge, where the keyword field would be unreachable. */
    max-height: calc(100vh - 5rem);
    overflow-y: auto;
    padding: 0.85rem 1rem;
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 0.6rem;
    background: rgba(0, 0, 0, 0.85);
}

/* Capture data: the same two-column list the shortcut legend uses, under its
   own name — a rule called "shortcuts" is not where the next person looks for
   the exposure row. */
.lightbox-capture {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.2rem 0.75rem;
    margin: 0;
    font-size: 0.8rem;
}

.lightbox-capture dt {
    color: #a3a3a3;
}

.lightbox-capture dd {
    margin: 0;
    color: #fafafa;
}

.lightbox-metadata-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
}

.lightbox-metadata-title {
    font-size: 0.8rem;
    font-weight: 600;
    color: #fafafa;
}

/* The content hash: one unbroken word of 32 characters. */
.lightbox-hash {
    font-size: 0.75rem;
    color: #a3a3a3;
    overflow-wrap: anywhere;
}

.lightbox-swatch {
    display: inline-block;
    width: 1.4rem;
    height: 1.4rem;
    border: 1px solid rgba(255, 255, 255, 0.25);
    border-radius: 50%;
}

/* The flag in the toolbar: a mark the size of a button, but not one. */
.lightbox-flag {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.1rem;
    height: 2.1rem;
    color: #ffffff;
}

.lightbox-keyword {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.15rem 0.5rem;
    border: 1px solid rgba(255, 255, 255, 0.25);
    border-radius: 999px;
    background: none;
    font-size: 0.8rem;
    color: #fafafa;
}

.lightbox-shortcuts-title {
    display: block;
    margin-bottom: 0.6rem;
    font-size: 0.8rem;
    font-weight: 600;
    color: #fafafa;
}

.lightbox-shortcuts-list {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    gap: 0.35rem 0.75rem;
    margin: 0;
    font-size: 0.8rem;
    color: #d4d4d4;
}

.lightbox-shortcuts-list dd {
    margin: 0;
}

.lightbox-shortcuts-list kbd {
    display: inline-block;
    min-width: 1.6rem;
    padding: 0.1rem 0.35rem;
    border: 1px solid rgba(255, 255, 255, 0.25);
    border-radius: 0.3rem;
    background: rgba(255, 255, 255, 0.08);
    font-family: inherit;
    font-size: 0.75rem;
    text-align: center;
    color: #fafafa;
}

/* Take-split editor */
.split-meta {
    display: flex;
    gap: 1rem;
    color: var(--muted-foreground);
    font-size: 0.875rem;
}

.split-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 22rem;
    gap: 1.5rem;
    align-items: start;
}

@media (max-width: 900px) {
    .split-layout {
        grid-template-columns: minmax(0, 1fr);
    }
}

.split-main {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    min-width: 0;
}

.split-video {
    width: 100%;
    max-height: 60vh;
    background: #000;
    border-radius: var(--radius);
}

.split-video-placeholder {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    aspect-ratio: 16 / 9;
    color: var(--muted-foreground);
    background: var(--muted);
    text-align: center;
    padding: 1rem;
}

.split-timeline {
    position: relative;
    width: 100%;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    cursor: pointer;
    user-select: none;
    background: var(--muted);
}

.split-filmstrip {
    display: block;
    width: 100%;
    height: 48px;
    object-fit: cover;
}

.split-waveform {
    display: block;
    width: 100%;
    height: 40px;
    object-fit: fill;
    opacity: 0.85;
}

.split-take-band {
    position: absolute;
    top: 0;
    bottom: 0;
    background: color-mix(in srgb, var(--primary) 28%, transparent);
    border-left: 2px solid var(--primary);
    border-right: 2px solid var(--primary);
    pointer-events: none;
}

.split-marker {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 2px;
    pointer-events: none;
}

.split-marker-in {
    background: var(--alert-success);
}

.split-marker-out {
    background: var(--destructive);
}

.split-playhead {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 2px;
    background: var(--foreground);
    pointer-events: none;
}

.split-hint {
    color: var(--muted-foreground);
    font-size: 0.8125rem;
    margin: 0;
}

.split-side {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.split-field-label {
    display: block;
    color: var(--muted-foreground);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

.split-time {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
}

.split-playhead-readout {
    margin-bottom: 0.75rem;
}

.split-inout {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
}

.split-snap {
    display: block;
    color: var(--muted-foreground);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
}

.split-buttons {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.split-empty {
    color: var(--muted-foreground);
    font-size: 0.875rem;
    margin: 0;
}

.split-take-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
}

.split-take-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0.375rem 0.5rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.split-take-remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.25rem;
    border: none;
    background: transparent;
    color: var(--muted-foreground);
    cursor: pointer;
    border-radius: var(--radius);
}

.split-take-remove:hover {
    color: var(--destructive);
    background: var(--muted);
}

/* Sign-in. Shared rather than page-local: the web host renders a standalone
   document (LoginPage) and the desktop host renders LoginView inline, and both
   link this stylesheet. Kept in one place so the two hosts cannot drift apart
   — they already say the same words through the same Login_* resources. */
.login-shell {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.5rem;
    background: var(--background);
    color: var(--foreground);
    font-family: var(--font-sans);
}

.login-card {
    width: 100%;
    max-width: 24rem;
    background: var(--card);
    color: var(--card-foreground);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 2rem;
}

.login-brand {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    margin-bottom: 1.75rem;
}

.login-brand img { width: 1.75rem; height: 1.75rem; }
.login-brand span { font-size: 1.125rem; font-weight: 600; }

.login-title {
    font-size: 1.25rem;
    margin: 0 0 1.25rem;
}

.login-field { margin-bottom: 1rem; }

.login-field label {
    display: block;
    margin-bottom: 0.375rem;
    font-size: 0.875rem;
    font-weight: 500;
}

.login-field input {
    width: 100%;
    padding: 0.5rem 0.75rem;
    font: inherit;
    color: var(--foreground);
    background: var(--input);
    border: 1px solid var(--border);
    border-radius: calc(var(--radius) - 2px);
}

.login-field input:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 1px;
}

.login-submit {
    width: 100%;
    margin-top: 0.5rem;
    padding: 0.5rem 1rem;
    font: inherit;
    font-weight: 500;
    color: var(--primary-foreground);
    background: var(--primary);
    border: none;
    border-radius: calc(var(--radius) - 2px);
    cursor: pointer;
}

/* The desktop disables the button for the duration of the attempt; without
   this it looks exactly like a button that simply ignores the click. */
.login-submit:disabled {
    opacity: 0.6;
    cursor: default;
}

.login-error {
    margin-bottom: 1.25rem;
    padding: 0.625rem 0.75rem;
    font-size: 0.875rem;
    color: var(--alert-danger-foreground);
    background: var(--alert-danger-bg);
    border: 1px solid var(--alert-danger);
    border-radius: calc(var(--radius) - 2px);
}

.login-note {
    margin-top: 1.5rem;
    font-size: 0.8125rem;
    color: var(--muted-foreground);
}

/* OAuth consent, rendered in the same card as the sign-in form and reusing its
   shell, field and error styles. Only the three things that form does not have
   are defined here: the sentence naming the application, the list of what it
   asks for, and a refuse button that must not look like the primary action. */
.consent-prompt {
    margin-top: 0;
    margin-bottom: 1.25rem;
}

.consent-asks {
    margin: 0 0 1rem;
    font-size: 0.9375rem;
}

.consent-scopes-title {
    margin-top: 0;
    margin-bottom: 0.5rem;
}

.consent-scopes {
    margin: 0 0 1.5rem;
    padding-left: 1.25rem;
    font-size: 0.875rem;
    color: var(--foreground);
}

.consent-deny {
    width: 100%;
    margin-top: 0.5rem;
    padding: 0.5rem 1rem;
    font: inherit;
    font-weight: 500;
    color: var(--foreground);
    background: transparent;
    border: 1px solid var(--border);
    border-radius: calc(var(--radius) - 2px);
    cursor: pointer;
}

.consent-deny:focus-visible,
.login-submit:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 1px;
}

/* Motion sensitivity: the operating-system setting wins. !important also
   reaches the component-scoped stylesheets, which the cascade would otherwise
   let outrank this file. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }

    /* The spinner is the only sign that a long ingest is still alive — slowed
       rather than stopped, because a frozen spinner reads as a hung app. */
    .loading-spinner {
        animation-duration: 2.4s !important;
        animation-iteration-count: infinite !important;
    }
}
