/* ==========================================================================
   Plugin surface — the house stylesheet a plugin's markup lands on.

   A plugin returns HTML and the host renders it inside the ordinary modal. It may reach for the
   app's own classes (btn, btn-primary, form-control, form-group, settings-hint, alert …) and get
   the design language for free, without knowing anything about it.

   Everything here is scoped under .plugin-surface on purpose: these rules must never leak into the
   application around the dialog. A plugin's own inline styles win over them, which is the intended
   escape hatch — a <style> block is not allowed at all, see PluginHtmlSanitizer.

   And they dress the plugin's own markup only: every rule for a bare element stops at a host widget
   (:where(:not(.plugin-host-widget *)), which adds no weight). The table, the editor and the recorder
   are the app's components with their own look — a plugin's <th> rule turned DataTable's headers into
   something else, and its <p> rule reached into the editor's paragraphs.
   ========================================================================== */

.plugin-surface {
    /* The height of a one-line field, and of a button standing beside one (.plugin-grid below). */
    --plugin-field-h: 2.5rem;

    display: flex;
    flex-direction: column;
    gap: 1rem;
    font-size: 0.9375rem;
    color: var(--text);
}

/* Headings inside a plugin dialog: quieter than the dialog's own title, which already names it. */
.plugin-surface h1:where(:not(.plugin-host-widget *)),
.plugin-surface h2:where(:not(.plugin-host-widget *)),
.plugin-surface h3:where(:not(.plugin-host-widget *)),
.plugin-surface h4:where(:not(.plugin-host-widget *)) {
    margin: 0;
    font-size: 1rem;
    font-weight: 600;
    color: var(--text);
}

.plugin-surface p:where(:not(.plugin-host-widget *)) {
    margin: 0;
    line-height: 1.5;
}

.plugin-surface small:where(:not(.plugin-host-widget *)),
.plugin-surface .hint:where(:not(.plugin-host-widget *)) {
    font-size: 0.8125rem;
    color: var(--text-muted);
}

/* A bare <label>Text <input></label> is the shortest thing a plugin can write and should already
   look right — the same shape .form-group produces, without needing the class.

   Not a label that is a button. A file picker is a <label> pointing at a hidden input, which is what
   PhotoCaptureField draws for "Bild hochladen" — stacking that one put its icon above its text and
   left it half a head taller than the "Kamera" button beside it. The exception is written as
   ":not(.btn)" rather than as a rule for that one component, because it holds for anything a host
   widget draws: whatever carries .btn is already a button and brings its own layout. */
.plugin-surface label:not(.btn):where(:not(.plugin-host-widget *)) {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--text);
}

.plugin-surface input:not([type="checkbox"]):not([type="radio"]):where(:not(.plugin-host-widget *)),
.plugin-surface select:where(:not(.plugin-host-widget *)),
.plugin-surface textarea:where(:not(.plugin-host-widget *)) {
    width: 100%;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
    font: inherit;
}

/* One height for every one-line field. Padding alone gave three: Chrome ignores the line height of a
   select, and a date field's calendar button is taller than text — side by side in a filter row the
   three edges stood at different heights. */
.plugin-surface input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="file"]):where(:not(.plugin-host-widget *)),
.plugin-surface select:not([multiple]):not([size]):where(:not(.plugin-host-widget *)) {
    height: var(--plugin-field-h);
    padding-top: 0;
    padding-bottom: 0;
}

.plugin-surface input:focus:where(:not(.plugin-host-widget *)),
.plugin-surface select:focus:where(:not(.plugin-host-widget *)),
.plugin-surface textarea:focus:where(:not(.plugin-host-widget *)) {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
}

.plugin-surface input[type="checkbox"]:where(:not(.plugin-host-widget *)),
.plugin-surface input[type="radio"]:where(:not(.plugin-host-widget *)) {
    accent-color: var(--accent);
}

/* Checkbox and radio rows read left-to-right, unlike the stacked label above. */
.plugin-surface label:not(.btn):has(> input[type="checkbox"]):where(:not(.plugin-host-widget *)),
.plugin-surface label:not(.btn):has(> input[type="radio"]):where(:not(.plugin-host-widget *)) {
    flex-direction: row;
    align-items: center;
    gap: 0.5rem;
    font-weight: 400;
}

.plugin-surface fieldset:where(:not(.plugin-host-widget *)) {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 0.75rem 1rem 1rem;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.plugin-surface legend:where(:not(.plugin-host-widget *)) {
    padding: 0 0.35rem;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

.plugin-surface table:where(:not(.plugin-host-widget *)) {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.875rem;
}

.plugin-surface th:where(:not(.plugin-host-widget *)),
.plugin-surface td:where(:not(.plugin-host-widget *)) {
    padding: 0.5rem 0.625rem;
    text-align: left;
    border-bottom: 1px solid var(--border);
}

.plugin-surface th:where(:not(.plugin-host-widget *)) {
    font-weight: 600;
    color: var(--text-muted);
    font-size: 0.8125rem;
}

.plugin-surface ul:where(:not(.plugin-host-widget *)),
.plugin-surface ol:where(:not(.plugin-host-widget *)) {
    margin: 0;
    padding-left: 1.25rem;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

.plugin-surface code:where(:not(.plugin-host-widget *)),
.plugin-surface pre:where(:not(.plugin-host-widget *)) {
    font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
    font-size: 0.8125rem;
    background: var(--surface-hover);
    border-radius: var(--radius-sm);
}

.plugin-surface code:where(:not(.plugin-host-widget *)) { padding: 0.1rem 0.35rem; }

/* Wide output scrolls inside its own box rather than widening the dialog. */
.plugin-surface pre:where(:not(.plugin-host-widget *)) {
    margin: 0;
    padding: 0.75rem;
    overflow-x: auto;
}

.plugin-surface img:where(:not(.plugin-host-widget *)) {
    max-width: 100%;
    height: auto;
    border-radius: var(--radius-sm);
}

.plugin-surface hr:where(:not(.plugin-host-widget *)) {
    width: 100%;
    border: none;
    border-top: 1px solid var(--border);
    margin: 0;
}

/* A button left in the body sits at the start of the line — it is part of the content there. */
.plugin-surface form > button,
.plugin-surface > button {
    align-self: flex-start;
}

/* The plugin's <footer>, rendered into the dialog's own action row. It joins the row rather than
   forming a second one, so "Abbrechen" and the plugin's buttons read as one set. */
.plugin-footer {
    display: contents;
}

/* ── What the browser carries out for a plugin ─────────────────────────────
   data-plugin-copy puts a text on the clipboard in the click itself (plugin-ui.js). The button says it
   worked for a moment, on the plugin's own markup — the plugin cannot, as nothing reaches it. */
[data-plugin-copy].is-copied::after,
[data-plugin-copy-from].is-copied::after {
    content: "✓";
    margin-left: 0.35rem;
    color: var(--clr-green-600);
}

/* The dictation widget takes the height the panel has (PluginPanel.razor.css makes the surface a
   column): the text grows in the middle, and the switches and the record button sit at the foot, where
   the thumb is — the panel's own action row would be there too. A plugin that draws more beside it
   gets the rest of the column; the memo plugin keeps its list on its page in the main menu. */
/* ── The action row ──
   A button marked plugin-start leaves the group at the right and stands at the left edge — a delete,
   a way back — so the row reads: what removes or returns on the left, what works with the content in
   the middle, how the dialog ends on the right. order moves it ahead of the host's own "Schließen",
   which may come first in the markup. Works in a dialog, the panel's foot and on a page alike: each
   lays the plugin's footer out as one flex row. */
.plugin-footer .plugin-start {
    order: -1;
    margin-right: auto;
}

/* In a plugin's dialog the host's "Schließen" joins the plugin's buttons in one row, and the order is
   set here, not by where each sits in the markup: what works with the content (0), then "Schließen" (1),
   then the plugin's own action — filled or danger — at the end (2). Without such an action "Schließen"
   is the end itself (is-last). PluginPanelDialogModal decides which. */
.plugin-dialog-actions .plugin-footer .btn-primary,
.plugin-dialog-actions .plugin-footer .btn-danger {
    order: 2;
}

.plugin-dialog-actions .plugin-close {
    order: 1;
}

.plugin-dialog-actions .plugin-close.is-last {
    order: 3;
}

/* ── A number field with − and + ──
   <input type="number" class="plugin-stepper"> — the host puts the two buttons around it
   (PluginHtmlSanitizer.DrawSteppers) and plugin-ui.js counts. They sit inside the field, as the lookup's
   glyph does, so the field keeps its own border, height and focus ring; the browser's own spinner goes,
   one pair of arrows is enough. Hover colours the glyph only, as on every icon inside a field. */
.plugin-stepper-box {
    position: relative;
    display: inline-flex;
    width: 8.5rem;
    max-width: 100%;
}

.plugin-surface .plugin-stepper-box > input.plugin-stepper[type="number"] {
    padding-left: 2.25rem;
    padding-right: 2.25rem;
    text-align: center;
    font-variant-numeric: tabular-nums;
    appearance: textfield;
    -moz-appearance: textfield;
}

.plugin-stepper-box > input.plugin-stepper::-webkit-inner-spin-button,
.plugin-stepper-box > input.plugin-stepper::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

.plugin-stepper-step {
    position: absolute;
    top: 0;
    bottom: 0;
    z-index: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    padding: 0;
    border: none;
    background: none;
    color: var(--text-muted);
    cursor: pointer;
    transition: color 0.15s;
}

.plugin-stepper-step:first-child { left: 0; }
.plugin-stepper-step:last-child { right: 0; }

.plugin-stepper-step:hover {
    color: var(--text);
}

/* A quiet delete: the text says what it does, the colour that it cannot be undone — without the fill
   that would make it the loudest button of a row it is not the point of. */
.btn.btn-ghost.plugin-danger {
    color: var(--clr-red-600);
}

.btn.btn-ghost.plugin-danger:hover:not(:disabled) {
    background: var(--clr-red-50);
    color: var(--clr-red-800);
}

/* The editor in a plugin's surface starts lower than the article editor's 320px: a memo of three
   sentences under a third of a screen of empty box looks unfinished. It grows with its text. */
.plugin-surface .plugin-editor .ql-editor {
    min-height: 10rem;
}

/* In a panel's dialog the editor is what the dialog is for: the dialog takes the height the screen has,
   up to about a page, and the editor everything its header, the note and the action row leave. On a
   phone the 10rem above floated in the middle of an empty screen. The text scrolls inside the editor,
   so its toolbar stays in reach; only where even the editor's minimum does not fit — the on-screen
   keyboard is up — does the dialog's body scroll instead. */
.dialog:has(> .plugin-dialog-body .plugin-editor) {
    height: min(100%, 46rem);
}

.plugin-dialog-body:has(.plugin-editor) {
    display: flex;
    flex-direction: column;
}

/* The surface sits in the wrapper that fades it while the plugin works (PluginPanelDialogModal): both fill. */
.plugin-dialog-body > .plugin-page-surface:has(.plugin-editor) {
    display: flex;
    flex: 1 1 0;
    flex-direction: column;
    min-height: 0;
}

.plugin-dialog-body .plugin-page-surface > .plugin-surface:has(> .plugin-editor) {
    flex: 1 1 0;
    min-height: 0;
}

/* The chain down to Quill's text box, each link filling the one above: RichTextEditor's host, the
   frame, Quill's container. The minimum sits on the outer one, so it holds for toolbar and text
   together whether the toolbar takes one row or two. */
.plugin-dialog-body .plugin-surface > .plugin-editor {
    flex: 1 1 0;
    min-height: 15rem;
    display: flex;
    flex-direction: column;
}

.plugin-dialog-body .plugin-editor .rte-host,
.plugin-dialog-body .plugin-editor .rte,
.plugin-dialog-body .plugin-editor .ql-container {
    flex: 1 1 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

.plugin-dialog-body .plugin-editor .rte-toolbar {
    flex: none;
}

.plugin-dialog-body .plugin-editor .ql-container {
    height: auto;
}

.plugin-dialog-body .plugin-editor .ql-editor {
    flex: 1 1 0;
    height: auto;
    min-height: 0;
    max-height: none;
}

/* The rule over a dialog's buttons marks the edge content scrolls under (ModalContainer.razor.css).
   Here the text scrolls inside the editor, whose frame already ends the content: a second line a
   rem below it only doubled the edge. A class deeper than that rule, which loads after this file. */
.dialog .plugin-dialog-body:has(.plugin-editor) + .dialog-actions {
    padding-top: 0;
    border-top: none;
}

/* ── A line of tools over the content ──
   What the line says first — a date, who it is shared with — and icon buttons at its end
   (btn-icon-box, named by title). Tools that work with the content stand by the content, so a
   dialog's action row keeps only how it ends: in the memo dialog Teilen, Kopieren, Drucken and
   Exportieren in the row made seven buttons, three rows on a phone. Where the line is too short the
   buttons wrap below it and still end at the right. */
.plugin-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.25rem;
}

.plugin-toolbar > :not(button) + button {
    margin-left: auto;
}

/* Options in a row stay flat (docs/DESIGN-SYSTEM.md §5a): the fill says they are buttons, a shadow
   under each of four would make the line louder than the text below it. */
.plugin-toolbar > .btn-icon-box,
.plugin-toolbar > .btn-icon-box:hover:not(:disabled),
.plugin-toolbar > .btn-icon-box:active:not(:disabled) {
    box-shadow: none;
}

/* ── Tabs ──
   The app's tab bar (AppTabs) is a component with script of its own, out of a plugin's reach. This is
   its look in plain markup: buttons in a rail, the open one on the navy pill —
   <div class="plugin-tabs"><button class="plugin-tab active">…</button>…</div>. Always the sunken
   rail the bar has in a dialog: a plugin's surface sits in a card already, on its page as well. No
   sliding pill and no overflow menu, both need the script; a tab that does not fit wraps onto a
   second row rather than going missing. */
.plugin-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 0;
    align-self: flex-start;
    max-width: 100%;
    padding: 0.3rem;
    background: var(--surface-sunken);
    border-radius: var(--radius-xl);
}

.plugin-tabs > .plugin-tab {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.5rem 0.95rem;
    background: none;
    border: none;
    border-radius: var(--radius-md);
    color: var(--text-secondary);
    font: inherit;
    font-size: 0.88rem;
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
    transition: color 0.15s, background-color 0.15s;
}

.plugin-tabs > .plugin-tab:not(.active):hover {
    background: color-mix(in srgb, var(--text) 7%, transparent);
    color: var(--text);
}

/* The open tab answers nothing — the plugin leaves its action off — so it does not look pressable. */
.plugin-tabs > .plugin-tab.active {
    background: var(--surface-inverse);
    color: var(--text-on-inverse);
    cursor: default;
}

/* The pill slides to the pressed tab while the plugin draws it (plugin-ui.js measures, as app-tabs.js
   does): the press answers at once, and the new drawing arrives with the pill where it already is.
   Until a press there is no slide, and the open tab's own fill above is the marker. */
.plugin-tabs {
    position: relative;
}

.plugin-tabs > .plugin-tab {
    position: relative;
    z-index: 1;
}

.plugin-tabs.is-sliding > .plugin-tab.active {
    background: transparent;
}

.plugin-tabs.is-sliding::after {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    width: var(--tab-w, 0);
    height: var(--tab-h, 0);
    transform: translate(var(--tab-x, 0), var(--tab-y, 0));
    background: var(--surface-inverse);
    border-radius: var(--radius-md);
    pointer-events: none;
    transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

@media (prefers-reduced-motion: reduce) {
    .plugin-tabs.is-sliding::after {
        transition: none;
    }
}

/* ── A list to choose from ──
   The app's status picker after a checklist milestone (SelectWorkOrderStatusModal) in plain markup: one
   bordered block of rows, each a button that answers at once — a choice that is a click, not a radio and
   a confirm. <div class="plugin-choices"><button class="plugin-choice" data-plugin-action="…">…</button></div>.
   What the row says goes first; a data-plugin-icon="chevron-right" ends it. */
.plugin-choices {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    overflow: hidden;
}

.plugin-choices > .plugin-choice {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0.6rem 0.75rem;
    background: var(--surface);
    border: none;
    border-radius: 0;
    color: var(--text);
    font: inherit;
    text-align: left;
    cursor: pointer;
    transition: background 0.15s;
}

.plugin-choices > .plugin-choice:not(:last-child) {
    border-bottom: 1px solid var(--clr-slate-100);
}

.plugin-choices > .plugin-choice:hover {
    background: var(--surface-hover);
}

.plugin-choices > .plugin-choice:focus-visible {
    outline: 2px solid var(--clr-blue-500);
    outline-offset: -2px;
}

/* What the row says takes the room, so the arrow ends it. */
.plugin-choice > :first-child {
    flex: 1;
    min-width: 0;
}

.plugin-choice > .icon:last-child,
.plugin-choice > :last-child > .icon {
    color: var(--clr-slate-300);
    flex-shrink: 0;
    transition: color 0.15s, transform 0.15s;
}

.plugin-choice:hover > .icon:last-child,
.plugin-choice:hover > :last-child > .icon {
    color: var(--clr-blue-500);
    transform: translateX(2px);
}

/* ── A plugin's surface while the plugin works on a press ──
   The page or dialog stays as it was drawn (PluginPage, PluginDialogModal, PluginRunModal), faded after a
   moment — most answers come back before anybody would notice, and a surface that blinked on every click
   would be worse than one that waited — with a spinner over it rather than "Fertig." where the list was,
   or a dialog that folds up to a spinner's height. The spinner is not faded: it is a sibling of what is. */
.plugin-page-surface {
    position: relative;
}

/* The delay on the way in only: the new drawing comes back at full strength at once. */
.plugin-page-surface > * {
    transition: opacity 0.15s ease;
}

.plugin-page-surface.is-working > :not(.plugin-page-wait) {
    opacity: 0.45;
    transition: opacity 0.2s ease 0.3s;
}

.plugin-page-wait {
    position: absolute;
    left: 50%;
    top: min(40%, 14rem);
    z-index: 3;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.65rem 1.1rem;
    border-radius: var(--radius-pill);
    background: var(--surface);
    box-shadow: var(--shadow-md);
    font-size: 0.875rem;
    color: var(--text-secondary);
    white-space: nowrap;
    transform: translateX(-50%);
    opacity: 0;
    animation: plugin-wait-in 0.2s ease 0.3s forwards;
}

@keyframes plugin-wait-in {
    to { opacity: 1; }
}

/* The plugin's buttons in the dialog's action row fade with the body they belong to. The footer itself
   is display: contents and has no opacity of its own to give. */
.plugin-footer > * {
    transition: opacity 0.15s ease;
}

.plugin-footer[inert] > * {
    opacity: 0.45;
    transition: opacity 0.2s ease 0.3s;
}

@media (prefers-reduced-motion: reduce) {
    .plugin-page-wait { animation-duration: 0.01s; }
}

/* ── A box that opens ──
   A bare <details> looks like the filter row of the app's lists (Expander): a quiet line with a
   chevron, closed until somebody opens it. With a name it answers whether it is open like a field
   (plugin-ui.js), so a plugin that draws its page anew after every press can draw it open again.
   <span class="plugin-count">2 aktiv</span> in the summary says what is narrowed while it is shut. */
.plugin-surface details:where(:not(.plugin-host-widget *)) {
    interpolate-size: allow-keywords;
}

/* Inline-flex, so only the label is the thing to click, not the empty rest of the line. The room
   to the content sits on the open summary rather than as a gap: a gap would stay under a shut box. */
.plugin-surface summary:where(:not(.plugin-host-widget *)) {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.75rem 0.5rem 0;
    list-style: none;
    font-size: 0.9rem;
    color: var(--clr-slate-500);
    cursor: pointer;
    transition: color 0.2s ease, margin-bottom 0.25s ease;
}

.plugin-surface details[open] > summary:where(:not(.plugin-host-widget *)) {
    margin-bottom: 0.75rem;
}

.plugin-surface summary:where(:not(.plugin-host-widget *))::-webkit-details-marker {
    display: none;
}

.plugin-surface summary:where(:not(.plugin-host-widget *)):hover {
    color: var(--clr-slate-700);
}

/* The chevron, drawn with two borders in the stroke weight of the app's icons: down while shut, up
   while open, as Expander's. */
.plugin-surface summary:where(:not(.plugin-host-widget *))::before {
    content: "";
    width: 0.4rem;
    height: 0.4rem;
    margin: 0 0.2rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: translateY(-0.12rem) rotate(45deg);
    transition: transform 0.2s ease;
}

.plugin-surface details[open] > summary:where(:not(.plugin-host-widget *))::before {
    transform: translateY(0.12rem) rotate(-135deg);
}

/* Opening and closing glide where the browser can animate to "auto"; elsewhere the box simply
   appears. Drawn open from the start — the plugin's redraw — it does not animate at all. */
.plugin-surface details:where(:not(.plugin-host-widget *))::details-content {
    block-size: 0;
    opacity: 0;
    overflow: clip;
    transition: block-size 0.25s ease, opacity 0.2s ease, content-visibility 0.25s allow-discrete;
}

.plugin-surface details[open]:where(:not(.plugin-host-widget *))::details-content {
    block-size: auto;
    opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
    .plugin-surface details:where(:not(.plugin-host-widget *))::details-content {
        transition: none;
    }
}

.plugin-surface .plugin-count {
    padding: 0.1rem 0.55rem;
    border-radius: var(--radius-pill);
    background: var(--primary-soft);
    color: var(--primary);
    font-size: 0.75rem;
    font-weight: 600;
    white-space: nowrap;
}

/* The surface's gap already spaces a notice from what follows; its own margin doubled it. */
.plugin-surface > .alert {
    margin-bottom: 0;
}

/* The same for a field group where its container spaces it already — the surface's column and the grid.
   The doubled edge put a branch's select twice as far from the remark under it as from its own label.
   Also when the group ends on a hint, which app.css gives a wider edge of its own. */
.plugin-surface > .form-group,
.plugin-surface > .form-group:has(> .form-hint:last-child),
.plugin-grid > .form-group,
.plugin-grid > .form-group:has(> .form-hint:last-child) {
    margin-bottom: 0;
}

/* An icon button has no room for the check beside its glyph: the check takes the glyph's place. */
.btn-icon-box.is-copied > svg {
    display: none;
}

.btn-icon-box[data-plugin-copy].is-copied::after,
.btn-icon-box[data-plugin-copy-from].is-copied::after {
    margin-left: 0;
}

/* A row that has just changed where the list comes from (class="plugin-changed" on the plugin's <tr>, which
   TableWidget hands to DataTable): it lights up once and settles. Once, because the row keeps its element
   while the list is drawn again — the class can stay until the next change without lighting it twice. */
.plugin-table tr.plugin-changed > td {
    animation: plugin-row-changed 2.4s ease-out;
}

@keyframes plugin-row-changed {
    from { background: var(--primary-soft); }
    to { background: transparent; }
}

@media (prefers-reduced-motion: reduce) {
    .plugin-table tr.plugin-changed > td {
        animation: none;
    }

    .plugin-table tr.plugin-changed > td:first-child {
        box-shadow: inset 3px 0 0 var(--primary);
    }
}

/* A row that answers a click (data-plugin-action on the <tr>, put on each cell by TableWidget) says so. */
.plugin-table td.plugin-row-action {
    cursor: pointer;
}

/* DataTable draws its own card, which is right on a list page and a card in a card here: a plugin's
   surface always sits in one already — the plugin page's, the panel, a dialog. The table keeps its
   scrolling and loses the frame. Three classes deep because the component's scoped stylesheet loads
   after this one. */
.plugin-surface .plugin-table .table-container {
    background: transparent;
    border: none;
    border-radius: 0;
    box-shadow: none;
    padding: 0;
}

/* DataTable sticks its header half a rem above the frame's edge, over the padding taken away just
   above — here that half rem was cut off the header instead. One pixel stays, as there. */
.plugin-surface .plugin-table .data-table thead {
    top: -1px;
}

/* On a plugin's page the list scrolls inside its frame, as every list page in the app does: the
   page is as high as the window, and the rows scroll under a header that stays — with the plugin's
   own pager and the action row beneath it in view. The chain from the page's card down to the
   frame has to let go of its height, link by link; only a frame's overflow lets it shrink.
   Not on a window too low for it, where a frame of a few rows would be worse than the page
   scrolling. */
@media (min-height: 560px) {
    .plugin-page-body:has(.plugin-table),
    .plugin-page-body:has(.plugin-table) .plugin-page-surface,
    .plugin-page-body .plugin-surface:has(> .plugin-table),
    .plugin-page-body .plugin-surface > .plugin-table {
        display: flex;
        flex-direction: column;
        min-height: 0;
    }

    .plugin-page-body .plugin-surface > .plugin-table > .table-container {
        min-height: 0;
        overflow-y: auto;
    }
}

.plugin-surface > .dictation {
    flex: 1;
    min-height: 16rem;
}

/* ── The states the host itself renders around the plugin ───────────────── */

.plugin-waiting,
.plugin-done {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    padding: 2rem 1rem;
    color: var(--text-muted);
    text-align: center;
}

/* `:global()` is Blazor's scoped-CSS syntax and means nothing in a plain stylesheet — this file is a
   global one (wwwroot/css/plugin.css), so the browser discarded both rules as invalid selectors and
   the spinner sat still. Plain descendant selectors, and the turning itself comes from the
   application's own `.icon-spin` (app.css) rather than from a second set of keyframes here: one
   animation for every loading indicator, including the reduced-motion opt-out that goes with it. */
.plugin-waiting svg {
    animation: icon-spin 1.1s linear infinite;
    transform-origin: 50% 50%;
}

.plugin-done svg {
    color: var(--clr-green-500, #22c55e);
}

@media (prefers-reduced-motion: reduce) {
    .plugin-waiting svg { animation: none; }
}

/* ── Fields side by side ───────────────────────────────────────────────────
   A plugin that reads a document ends up with twenty fields, and stacked one under another that is
   a dialog nobody can see the end of. This is the house answer, so no plugin has to invent one; how
   dense such a form is, and how its sections are headed, is the plugin's own (rezepte.ts brings it in
   a style block of its own). The grid needs no media query: it reflows on its own width, which is the
   right measure anyway — the dialog is narrower than the viewport. */

/* Fields side by side, as many as fit. auto-fit collapses to one column on a phone without a media
   query, and minmax keeps a column wide enough for a date or an insurance number to stay readable. */
.plugin-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
    gap: 0.75rem 1rem;
    align-items: start;
}

/* Something that belongs across the whole row inside a grid — a heading, a note, a wide field. */
.plugin-grid .plugin-wide {
    grid-column: 1 / -1;
}

/* A cell without a caption — a button beside the fields ("Suchen"), a checkbox — stands on the
   fields' line instead of at the top of its row, where it sat a label's height above the boxes it
   belongs to. As high as a field, so their middles meet as well as their edges. */
.plugin-grid > :has(> .btn:only-child),
.plugin-grid > :has(> label:only-child > input[type="checkbox"]),
.plugin-grid > :has(> label:only-child > input[type="radio"]) {
    align-self: end;
}

.plugin-grid > :has(> .btn:only-child) > .btn {
    height: var(--plugin-field-h);
    padding-top: 0;
    padding-bottom: 0;
}

.plugin-grid > :has(> label:only-child > input[type="checkbox"]) > label,
.plugin-grid > :has(> label:only-child > input[type="radio"]) > label {
    min-height: var(--plugin-field-h);
    margin-bottom: 0;
}

/* The manifest card's facts: label above value, all values on one left edge. The rhythm is a
   little roomier than a work order's detail block because there are only two of them. */
.plugin-facts {
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
}

.plugin-facts .detail-field dd {
    margin: 0;
}

/* Monaco has no intrinsic height: it measures whatever box it is put in. BlazorMonaco renders its
   own wrapper div between this element and the editor, and that wrapper is what collapses — the
   editor then draws one clipped line into an otherwise empty box.

   A single-row grid makes the wrapper stretch to the full height without having to know its tag or
   class, which is what a `height: 100%` chain would need. */
.plugin-code-editor {
    display: grid;
    grid-template-rows: 1fr;
    height: 32rem;
    min-height: 16rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    overflow: hidden;
}

/* Grid items default to min-height:auto, which would let the editor push the row taller than the
   box instead of scrolling inside it. */
.plugin-code-editor > * {
    min-height: 0;
    min-width: 0;
}

@media (max-width: 768px) {
    .plugin-code-editor { height: 22rem; }
}

/* A plugin's files, one tab each, over the editor they are opened in — the look of an editor's tabs
   rather than the app's pill: they name files, and a file name is read as code. The tools for the open
   file stand at the row's end. */
.plugin-file-tabs {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 0.15rem;
    border-bottom: 1px solid var(--border);
}

.plugin-file-tab {
    padding: 0.4rem 0.8rem;
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    color: var(--text-secondary);
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: 0.82rem;
    cursor: pointer;
}

.plugin-file-tab:hover {
    color: var(--text);
}

.plugin-file-tab.active {
    border-bottom-color: var(--accent);
    color: var(--text);
    font-weight: 600;
    cursor: default;
}

.plugin-file-tools {
    display: flex;
    align-items: center;
    gap: 0.1rem;
    margin-left: auto;
    padding-bottom: 0.2rem;
}

.plugin-file-form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0;
}

.plugin-file-form .form-control {
    width: auto;
    min-width: 14rem;
    font-family: var(--font-mono, ui-monospace, monospace);
}

/* ── The lookup (data-plugin-widget="lookup", LookupWidget) ────────────────
   SearchCombobox's look in a form: a field like the plugin's others, with the search glyph in front, and
   its hits floating under it. The hits are the plugin's buttons; each one reads as a row of the house
   list — its text the title, a <small> in it the line under the title. The list is fixed and placed by
   flowklarPopup.follow, which also makes it visible once it stands where it belongs. */
.plugin-lookup {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.plugin-lookup-label {
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--text);
}

.plugin-lookup-field {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    height: var(--plugin-field-h);
    padding: 0 0.75rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    transition: border-color 0.15s, box-shadow 0.15s;
}

.plugin-lookup-field:focus-within {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
}

.plugin-lookup-icon {
    display: inline-flex;
    color: var(--clr-slate-400);
    flex-shrink: 0;
}

.plugin-lookup-input {
    flex: 1;
    min-width: 0;
    height: 100%;
    border: none;
    outline: none;
    background: transparent;
    color: var(--text);
    font: inherit;
}

.plugin-lookup-list {
    position: fixed;
    visibility: hidden;
    z-index: 9999;
    display: none;
    flex-direction: column;
    gap: 0.1rem;
    max-width: calc(100vw - 1rem);
    max-height: 290px;
    padding: 0.3rem;
    overflow-y: auto;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-md);
    scrollbar-width: thin;
    animation: plugin-lookup-in 0.12s ease-out;
}

.plugin-lookup.is-open .plugin-lookup-list {
    display: flex;
}

@keyframes plugin-lookup-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

.plugin-lookup-list > button {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.05rem;
    flex-shrink: 0;
    width: 100%;
    padding: 0.45rem 0.6rem;
    border: none;
    border-radius: var(--radius-sm);
    background: none;
    font: inherit;
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--clr-slate-800);
    text-align: left;
    cursor: pointer;
    transition: background 0.12s;
}

.plugin-lookup-list > button:hover,
.plugin-lookup-list > button.is-active {
    background: var(--surface-hover);
}

.plugin-lookup-list > button:disabled {
    cursor: default;
    opacity: 0.6;
}

.plugin-lookup-list > button small {
    font-size: 0.75rem;
    font-weight: 400;
    color: var(--clr-slate-500);
}

.plugin-lookup-list > p,
.plugin-lookup-empty {
    margin: 0;
    padding: 0.5rem 0.6rem;
    font-size: 0.85rem;
    color: var(--clr-slate-400);
}

/* ── A way to one of the plugin's pages ──
   A row or a card that names a page with data-plugin-route (the host writes data-plugin-href) leads there on
   a click, as a link would, and says so. A table's rows carry the same through TableWidget. */
[data-plugin-href] {
    cursor: pointer;
}

/* ── The map (data-plugin-widget="map", MapWidget) ──
   The address picker's frame: the sunken surface while the tiles load, the radius of a field. The points are
   the plugin's, numbered; the line through them is the tour's way, in the primary blue. Leaflet's panes sit at
   z-index 400 and up, so the frame starts a stacking context of its own and stays under the dialog's footer. */
.plugin-map-canvas {
    position: relative;
    z-index: 0;
    width: 100%;
    border-radius: var(--radius-lg);
    overflow: hidden;
    background: var(--surface-sunken);
    box-shadow: var(--shadow-xs);
}

.plugin-map-frame {
    position: relative;
}

/* The tiles' credit, which their licence asks to be visible: glass in the map's corner, the tiles blurred
   behind it (app.css, --surface-menu); solid where backdrop-filter is missing. */
.plugin-map-credit {
    position: absolute;
    right: 0.5rem;
    bottom: 0.5rem;
    z-index: 1;
    max-width: calc(100% - 1rem);
    padding: 0.15rem 0.5rem;
    border-radius: var(--radius-sm);
    background: var(--surface);
    background: var(--surface-menu);
    -webkit-backdrop-filter: var(--menu-blur);
    backdrop-filter: var(--menu-blur);
    box-shadow: var(--shadow-xs);
    color: var(--text-secondary);
    font-size: 0.7rem;
    line-height: 1.4;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.plugin-map .plugin-map-point {
    background: none;
    border: none;
}

.plugin-map .plugin-map-point > span {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: var(--primary);
    border: 2px solid var(--surface);
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
    color: #fff;
    font-size: 0.75rem;
    font-weight: 700;
}

.plugin-map .plugin-map-point.tone-accent > span { background: var(--accent); color: var(--clr-slate-900); }
.plugin-map .plugin-map-point.tone-muted > span { background: var(--clr-slate-400); }
.plugin-map .plugin-map-point.tone-warning > span { background: var(--clr-amber-500); color: var(--clr-slate-900); }
.plugin-map .plugin-map-point.tone-danger > span { background: var(--danger); }
.plugin-map .plugin-map-point.tone-success > span { background: var(--clr-emerald-500); }

.plugin-map .plugin-map-point.is-clickable {
    cursor: pointer;
}

.plugin-map .plugin-map-point.is-clickable:hover > span {
    transform: scale(1.12);
}

.plugin-map .plugin-map-line {
    stroke: var(--primary);
    stroke-opacity: 0.7;
}

/* ── The sortable list (data-plugin-widget="sortable", SortableWidget) ──
   Rows like a list's, each with its handle in front. The handle is the only thing that starts a drag, so the
   plugin's buttons inside an item stay buttons; it takes the focus, and ↑ ↓ move its item. While one is in the
   air the others slide aside — transform only, the elements stay where Blazor put them. */
.plugin-sortable-list {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.plugin-sortable-item {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
    padding: 0.55rem 0.7rem;
    background: var(--surface);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-xs);
    transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}

.plugin-sortable-item.is-dragging {
    position: relative;
    z-index: 1;
    box-shadow: var(--shadow-sm);
    transition: none;
}

.plugin-sortable-list.is-sorting {
    user-select: none;
}

.plugin-sortable-handle {
    display: flex;
    flex: none;
    align-items: center;
    padding: 0.2rem;
    background: none;
    border: none;
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    cursor: grab;
    touch-action: none;
}

.plugin-sortable-handle:hover,
.plugin-sortable-handle:focus-visible {
    color: var(--primary);
}

.plugin-sortable-item.is-dragging .plugin-sortable-handle {
    cursor: grabbing;
}

.plugin-sortable-body {
    flex: 1;
    min-width: 0;
}

@media (prefers-reduced-motion: reduce) {
    .plugin-sortable-item {
        transition: none;
    }
}

/* ── A search line and filter groups ──
   The look of the app's lists in plain markup: SearchBar (a field with the magnifier in it, a shadow rather
   than a border, not across a whole wide page) with the plugin's "Suchen" beside it, and inside a list's
   <details name="filter"> the Expander's filter groups — a small capitalised caption over a row of small
   buttons, the chosen one filled:

     <div class="plugin-search" data-plugin-enter="suchen">
       <span class="plugin-search-icon" data-plugin-icon="search"></span>
       <input class="plugin-search-input" name="suche" placeholder="…">
       <button class="btn btn-secondary" data-plugin-action="suchen">Suchen</button>
     </div>
     <div class="plugin-filters">
       <div class="plugin-filter-group">
         <span class="plugin-filter-label">Status</span>
         <div class="plugin-filter-buttons"><button class="btn btn-sm btn-primary" …>Offen</button>…</div>
       </div>
     </div>

   A select or a date in a group takes effect when it is changed (data-plugin-change, plugin-ui.js), as the
   app's filters do — no "Suchen" for those. */
.plugin-search {
    --plugin-field-h: 2.75rem;
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    max-width: 42rem;
}

.plugin-search > .plugin-search-icon {
    position: absolute;
    left: 0.85rem;
    top: 50%;
    z-index: 1;
    display: flex;
    color: var(--clr-slate-400);
    pointer-events: none;
    transform: translateY(-50%);
}

.plugin-search > .plugin-search-icon svg {
    width: 16px;
    height: 16px;
}

/* As specific as the surface's own field rules, and after them: the field keeps their focus ring. */
.plugin-surface .plugin-search input.plugin-search-input {
    flex: 1;
    min-width: 0;
    padding-left: 2.6rem;
    border-color: transparent;
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    font-size: 0.95rem;
}

.plugin-search > .btn {
    flex: none;
    height: var(--plugin-field-h);
}

.plugin-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem;
    padding-bottom: 0.25rem;
}

.plugin-filter-group {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.plugin-filter-label {
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--clr-slate-400);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.plugin-filter-buttons {
    --plugin-field-h: 2rem;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.875rem;
    color: var(--text-muted);
}

/* A select or a date in a group: as high as the buttons beside it, as wide as what it holds. */
.plugin-surface .plugin-filter-buttons > select,
.plugin-surface .plugin-filter-buttons > input {
    width: auto;
    min-width: 9rem;
    font-size: 0.875rem;
}

@media (max-width: 768px) {
    .plugin-filters {
        flex-direction: column;
        gap: 1rem;
    }

    .plugin-search {
        max-width: none;
    }
}
