:root {
    --tv-default-border-radius: 0.5rem;

    /* Width of the docked AI chat panel. Shared by the panel itself and by the
       margin-right that shifts .main-content out from under it — they must match. */
    --ai-chat-docked-width: min(400px, 35vw);

    /* ── Tier 1: typography ──
       The app used to inherit the stock `dotnet new blazor` stack
       ('Helvetica Neue', Helvetica, Arial). Neither Helvetica face exists on Windows, so
       app-authored text fell through to Arial while every Syncfusion control rendered in the
       theme's own stack — two typefaces with different x-heights and metrics on every screen.
       This mirrors Bootstrap 5.3's native stack so app text and component text match.

       Font *family* is set globally; the size/weight tokens below are applied deliberately by
       components. Setting a global font-size would reflow every existing screen, and TimeView is
       a dense power-user tool where that density is a feature. */
    --tv-font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
                    "Noto Sans", "Liberation Sans", Arial, sans-serif,
                    "Apple Color Emoji", "Segoe UI Emoji";

    /* Machine output — trace ids, request ids, stack traces. Mirrors Bootstrap 5.3's own
       monospace stack for the same reason as the sans stack above. */
    --tv-font-mono: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
                    "Courier New", monospace;

    /* Type scale. Five steps, because five is what the screens actually need:
       page title, section heading, body, supporting metadata, and the large numerals
       on KPI/summary cards. Resist adding a sixth without a screen that needs it. */
    --tv-text-page-title: 1.375rem;
    --tv-text-section:    1rem;
    --tv-text-body:       0.875rem;
    --tv-text-meta:       0.75rem;
    --tv-text-metric:     1.75rem;

    --tv-weight-regular: 400;
    --tv-weight-medium:  500;
    --tv-weight-strong:  600;

    --tv-leading-tight:  1.25;
    --tv-leading-normal: 1.5;

    /* ── Tier 1: spacing ──
       A 4px base step. Kept short on purpose: page gutter and section gap are the two values
       that were missing everywhere (grids ran edge-to-edge with no gutter), the numbered steps
       cover component-internal spacing. */
    --tv-space-1: 0.25rem;
    --tv-space-2: 0.5rem;
    --tv-space-3: 0.75rem;
    --tv-space-4: 1rem;
    --tv-space-5: 1.5rem;
    --tv-space-6: 2rem;

    --tv-page-gutter: var(--tv-space-4);
    /* Gap between the app bar and page content. Shared with .top-bar's sticky `top` so the bar
       rests and sticks at the same offset instead of jumping up when scrolling starts. */
    --tv-content-top-gap: 0.2rem;
    --tv-section-gap: var(--tv-space-5);

    /* ── Tier 2: focus ──
       Two-tone ring: a thin separating ring against the element, then the accent.
       The separator uses the theme's own background so it reads on light and dark
       alike (the stock template hardcoded `white`, which vanished on dark panels). */
    --tv-focus-ring: var(--tv-accent);
    --tv-focus-ring-inner: var(--color-sf-content-bg-color, #ffffff);

    /* ── Tier 1: primitive palette ─────────────────────────────────────────
       Interface hues, named by hue. Every semantic token below points here, so a hue is changed
       in exactly one place. Do not reference these directly from component styles — use a
       semantic token instead.

       These are *not* the TimeSolutions brand colours, despite an earlier version of this comment
       claiming they were. They are the theme's semantic hues — what "info", "success", "warning"
       and "danger" look like in the active Syncfusion theme, with a Material/Bootstrap hex as the
       fallback. That is the right basis for status meaning: a status colour has to track the theme
       and hold contrast as a small glyph, which a brand colour chosen for a logo does not.
       The brand palette is defined separately below and is not interchangeable with these.

       Where a hue has a natural match in Syncfusion's semantic theme variables,
       it inherits from the active theme (var(--color-sf-*, <brand fallback>)) so
       status colors track theme switches (light/dark/high-contrast/Fluent/etc.).
       The brand hex is the fallback for themes that don't define the variable
       (e.g. bootstrap5, highcontrast). Hues with no semantic Syncfusion match
       (grey/orange/black) stay explicit.
       Caveat: some Syncfusion themes define --color-sf-* as raw RGB triplets
       ("r, g, b") rather than full colors, so on those the token resolves to an
       invalid value and the element falls back to its inherited color. The
       supported family (bootstrap5.3) defines them normally. */
    --palette-grey:   #757575;
    --palette-blue:   var(--color-sf-info,    #4dadff);
    --palette-green:  var(--color-sf-success, #2e7d32);
    --palette-amber:  var(--color-sf-warning, #f9a825);
    --palette-red:    var(--color-sf-danger,  #c62828);
    --palette-accent: var(--color-sf-primary, #0d6efd);
    --palette-orange: #ff814c;
    --palette-black:  #000000;

    /* Categorical hues. No semantic Syncfusion match, and none is wanted: these exist to tell
       taxonomy entries apart at a glance, so they must stay mutually distinct rather than track a
       theme's notion of "info" or "success". */
    --palette-purple: #6a1b9a;
    --palette-teal:   #00695c;

    /* Deep variants, for use as a solid *fill* behind white text. The theme-derived hues above are
       tuned for glyphs and borders on the page background; as a background they leave white text at
       roughly 2:1, so they cannot simply be reused. Explicit hex on purpose — a theme-inherited
       value gives no guarantee of the contrast a filled chip needs. */
    --palette-green-deep: #1b5e20;
    --palette-amber-deep: #a35200;

    /* ── Tier 1: TimeSolutions brand palette ───────────────────────────────
       The canonical corporate identity, from the 2022 design guide (ImageConsult,
       "TimeSolutions _ designguide 13_01_22.pdf", p. 8 — the RGB set, which the guide
       specifies for on-screen use; the CMYK and Pantone sets on pp. 7 and 9 are for print).

       These are fixed corporate values. Unlike --palette-* above they deliberately do NOT
       inherit from the Syncfusion theme: the brand green is the brand green in light, dark and
       high contrast alike. That is also why they must be used sparingly — a fixed colour cannot
       guarantee contrast against a surface it does not control.

       Use for: identity surfaces — sign-in, the app shell's brand mark, marketing/empty states,
       print and export headers, illustration and chart accents where the palette is decorative,
       and structural chrome that divides a screen (the EntityDetail section bands).

       Do NOT use for: status, validation or any colour that carries meaning. Those belong to the
       --palette-* hues above and their Tier-2 semantic tokens. Brand blue reading as "info" or
       brand green as "success" is a coincidence of hue, not a decision, and it breaks the moment
       the identity is refreshed. (One documented exception: --approval-employee-closed, below.)

       Contrast, computed (not eyeballed) against white and against the dark theme's #212529:

                     on white   on #212529
         GREEN         2.78        5.55
         COOLGREEN     3.26        4.73
         TURQUOISE     2.95        5.24
         BLUE          5.74        2.69
         DARKBLUE     11.98        1.29

       So there is no brand hue that is legible as text on both themes — BLUE and DARKBLUE work
       on light only, the three greens on dark only. Worse, GREEN (2.78) and TURQUOISE (2.95) sit
       under even the 3:1 floor for icons and borders on white. Treat every one of these as a
       *fill* colour with a deliberately chosen foreground on top, and check the pairing in the
       browser; do not reuse these numbers for a surface other than the two above. */
    --brand-green:     #50af43;
    --brand-coolgreen: #00a278;
    --brand-turquoise: #00a6b6;
    --brand-blue:      #1768b2;
    --brand-darkblue:  #19375f;
    --brand-black:     #000000;

    /* Neutral ramp. The guide specifies black at 100/75/50/25% plus white; these are those
       percentages flattened against white, so they stay usable as opaque fills. On a dark
       surface prefer the --tv-* neutral chrome tokens below, which track the theme. */
    --brand-black-75: #404040;
    --brand-black-50: #808080;
    --brand-black-25: #bfbfbf;
    --brand-white:    #ffffff;

    /* The guide encourages gradients between brand hues ("forløb kan bruges", p. 8) for
       identity surfaces and line icons. Named here so the pairing is decided once rather than
       improvised per screen. Not for use behind body text. */
    --brand-gradient-cool: linear-gradient(135deg, var(--brand-green), var(--brand-turquoise));
    --brand-gradient-deep: linear-gradient(135deg, var(--brand-turquoise), var(--brand-darkblue));

    /* ── Tier 2: chart series ──
       Fills for data series that carry NO meaning — external vs internal hours, posted vs pipeline
       revenue, a top-N breakdown. Those were painted with status hues (--palette-blue/-orange, the
       invoice status tokens), so every chart read as an alert and none of the brand appeared below
       the page header. The design guide lists chart accents as a legitimate decorative use of the
       identity, so the series ramp is the brand palette, in a fixed order.

       Order is by legibility as a fill, not by the guide's listing: BLUE first (already the app's
       primary — 5.74:1 light, 2.69 dark, the trade the primary override in theme-overrides.css
       already accepts), then COOLGREEN, the one brand hue that clears the 3:1 graphics floor on
       both themes (3.26 / 4.73). Series 3–5 dip under 3:1 on one theme each and are meant for
       charts that also carry a legend and data labels, not for a lone bar.

       A series that DOES mean something — a credit note, an overdue bucket, the norm line, an
       approval state — keeps its semantic token. Don't put one of those beside a brand series in
       the same chart unless it is neutral (the grey norm line is fine): a brand hue next to a red
       or amber makes the brand hue look like a status too. */
    --chart-series-1: var(--brand-blue);
    --chart-series-2: var(--brand-coolgreen);
    --chart-series-3: var(--brand-turquoise);
    --chart-series-4: var(--brand-green);
    --chart-series-5: var(--brand-darkblue);

    /* ── Tier 2: AI instruction categories ──
       A filled chip per instruction category in the AI admin list. Deep variants throughout,
       because the category label is white text on the fill. Chosen for mutual distinctness, not
       for meaning — none of these categories is "good" or "bad". */
    --ai-category-persona:         var(--palette-accent);
    --ai-category-workflow:        var(--palette-green-deep);
    --ai-category-domain-skill:    var(--palette-amber-deep);
    --ai-category-conditional:     var(--palette-purple);
    --ai-category-prompt-template: var(--palette-teal);

    /* ── Tier 2: entity identity ──
       The colour of an entity's glyph wherever the entity is drawn — the navigation menu today.

       Every entity currently resolves to the brand primary: a menu in thirty hues reads as
       decoration rather than as structure, and the glyph shape already says which entity it is.
       The per-entity indirection is kept anyway, so singling one out later is a one-line change
       here — repoint --entity-invoice at a --palette-* primitive and nothing else moves.

       Referenced from EntityConfiguration.IconColor; see the "Entity-List Status Icons" section of
       .github/copilot-instructions.md. */
    --tv-nav-icon: var(--palette-accent);

    /* The selected menu row is filled with the brand primary, so a glyph in --tv-nav-icon would be
       invisible against it. This is the foreground the theme already pairs with that fill, so the
       glyph tracks the row's own text rather than needing a second colour decision. */
    --tv-nav-icon-selected: var(--color-sf-primary-text-color, var(--brand-white));

    /* Customers and the taxonomies that classify them. */
    --entity-customer:              var(--tv-nav-icon);
    --entity-customertype:          var(--tv-nav-icon);
    --entity-companytype:           var(--tv-nav-icon);
    --entity-contact:               var(--tv-nav-icon);
    --entity-contactgroup:          var(--tv-nav-icon);
    --entity-referencerole:         var(--tv-nav-icon);

    /* Work: what is done, and how it is classified and registered. */
    --entity-tvtask:                var(--tv-nav-icon);
    --entity-tvtaskdeadline:        var(--tv-nav-icon);
    --entity-tasktype:              var(--tv-nav-icon);
    --entity-worktype:              var(--tv-nav-icon);
    --entity-timeregistration:      var(--tv-nav-icon);

    /* Money. */
    --entity-invoice:               var(--tv-nav-icon);
    --entity-invoiceline:           var(--tv-nav-icon);
    --entity-invoicerule:           var(--tv-nav-icon);
    --entity-currencyexchangerate:  var(--tv-nav-icon);
    --entity-accountingsystem:      var(--tv-nav-icon);

    /* People, and the contracts and calendars that govern their time. */
    --entity-tvuser:                var(--tv-nav-icon);
    --entity-tvgroup:               var(--tv-nav-icon);
    --entity-tvusersgroup:          var(--tv-nav-icon);
    --entity-tvuserrateinterval:    var(--tv-nav-icon);
    --entity-usercontract:          var(--tv-nav-icon);
    --entity-usercontracttemplate:  var(--tv-nav-icon);
    --entity-workday:               var(--tv-nav-icon);
    --entity-workdaystemplate:      var(--tv-nav-icon);
    --entity-holiday:               var(--tv-nav-icon);
    --entity-flexvacationinfo:      var(--tv-nav-icon);

    /* Documents and things that are sent. */
    --entity-correspondence:        var(--tv-nav-icon);
    --entity-correspondencecategory: var(--tv-nav-icon);
    --entity-standardtext:          var(--tv-nav-icon);
    --entity-reportdefinition:      var(--tv-nav-icon);
    --entity-emaillog:              var(--tv-nav-icon);

    /* Configuration of the product itself, rather than of the business it records. */
    --entity-customfielddefinition: var(--tv-nav-icon);
    --entity-auditlog:              var(--tv-nav-icon);

    /* ── Tier 2: navigation entries with no entity behind them ──
       A menu item that is a *place* (a calendar, an administration section) rather than a list of
       one entity. Kept beside the entity tokens so the menu has one palette, not two, and pointing
       at the same --tv-nav-icon for the same reason.
       Referenced from NavMenuIcons. */
    --nav-home:             var(--tv-nav-icon);
    --nav-planning:         var(--tv-nav-icon);
    --nav-documenttemplate: var(--tv-nav-icon);
    --nav-hr:               var(--tv-nav-icon);
    --nav-administration:   var(--tv-nav-icon);
    --nav-system:           var(--tv-nav-icon);
    --nav-debug:            var(--tv-nav-icon);

    /* ── Tier 2: neutral chrome ──
       Derived from the element's own foreground rather than a fixed light grey, so
       surfaces and separators stay legible on light, dark and high-contrast themes.
       (currentColor is resolved where the token is used, not here.) */
    --tv-border-color: color-mix(in srgb, currentColor 25%, transparent);
    --tv-surface-alt: color-mix(in srgb, currentColor 8%, transparent);

    /* Same idea, under the names components already reference. Both were used across the UI with
       hardcoded light-theme hex fallbacks (#555 / #d0d0d0 / #595959) and defined nowhere, so every
       use silently fell back — including on dark themes, where a mid-grey on a dark panel fails
       contrast. Deriving from currentColor makes them track the active theme. */
    --tv-color-text-secondary: color-mix(in srgb, currentColor 70%, transparent);
    --tv-color-border: var(--tv-border-color);

    /* Hover wash for list rows, menu items and icon buttons. Previously referenced as
       --tv-color-hover with a hardcoded #f5f5f5 fallback and defined nowhere, so every hover
       painted light grey — invisible on light themes, wrong on dark ones. */
    --tv-color-hover: var(--color-sf-hover-bg-color, color-mix(in srgb, currentColor 10%, transparent));

    /* Opaque panel surface for anything that floats above page content (flyouts, cards,
       popups). Components previously reached for the undefined --e-bg-color with a #ffffff
       fallback, which pinned them to white on every theme. */
    --tv-surface: var(--color-sf-content-bg-color, #ffffff);

    /* Page canvas. Deliberately a step darker than --tv-surface rather than equal to it: the
       layout relies on cards/panels reading as raised against the canvas, and painting both from
       --color-sf-content-bg-color erases that distinction. Mixing toward black (not toward the
       text colour) keeps the canvas darker than the surface on light *and* dark themes.
       Overridden for dark in theme-overrides.css — see the note there. */
    --tv-body-bg: color-mix(in srgb, var(--color-sf-content-bg-color, #ffffff) 96%, #000000);
    --tv-body-fg: var(--color-sf-content-text-color, #333333);

    /* Elevation for those same floating panels. A shadow, not a colour, so it has no palette
       entry — but it was being duplicated inline per component via an undefined --e-shadow. */
    --tv-shadow-flyout: 0 0.5rem 2rem rgba(0, 0, 0, 0.1);

    /* Raised variant of the above, for hover on a floating card. Same story: it was inlined per
       component as an undefined --e-shadow-hover. */
    --tv-shadow-flyout-hover: 0 1rem 3rem rgba(0, 0, 0, 0.15);

    /* ── Tier 2: accent fill and its foreground ──
       Syncfusion themes expose --color-sf-primary but no matching "on primary"
       variable, so a component that fills itself with the accent has nothing to
       read for its foreground. Declared once here rather than hardcoded per
       component. White holds up against the accent on every bundled theme; a
       theme with a light primary would override this token, not the components. */
    --tv-accent: var(--palette-accent);
    --tv-on-accent: #ffffff;

    /* ── Tier 2: in-place search ──
       Marks the rows a client-side search matched (e.g. the batch-edit field list).
       Amber rather than the primary colour so a hit never reads as a selection. */
    --tv-search-match: var(--palette-amber);

    /* ── Tier 2: global search ──
       The app-bar search box and its results panel. The panel floats above page content, so it
       needs a surface of its own rather than inheriting whatever it happens to overlap. Values
       come from the Syncfusion theme so the panel follows a theme switch; the hexes are only
       fallbacks for themes that define neither. */
    --tv-search-surface: var(--color-sf-flyout-bg-color, var(--color-sf-content-bg-color, #ffffff));
    /* The bar's own border variable is too faint against the app-bar background to register as an
       edge, so the field's outline is drawn from the text colour like every other input border in
       the app (`--tv-border-color`); the results panel keeps the theme's lighter border. */
    --tv-search-border: var(--color-sf-border, rgba(128, 128, 128, .35));
    --tv-search-field-border: var(--tv-border-color);
    --tv-search-hover: var(--color-sf-hover-bg-color, rgba(128, 128, 128, .12));
    /* Demoted rows: ended employees, inactive/blocked customers, archived tasks. Dimmed rather
       than hidden — they are still results, just not the ones you probably meant. */
    --tv-search-muted: var(--palette-grey);

    /* ── Tier 2: entity detail form ──
       The generic edit form (EntityDetail) used to inherit Bootstrap 5.3's accordion styling
       verbatim, which paints an expanded section header as a full-bleed tinted bar — a solid
       blue band on light, navy on dark, and the theme's own link blue at that. These tokens
       repaint the form as a stack of cards, each headed by a solid band in the TimeSolutions
       BLUE. The band is deliberately loud — it is the only chrome in the form, so it is what
       divides the page into sections; everything below it (fields, labels, borders) stays
       neutral and derived from the Syncfusion theme, so light, dark and high contrast follow.

       This is a *branded* fill, not a status colour, so it is exempt from the "never use
       --brand-* for meaning" rule above: the band says "section", not "approved" or "error".
       It is also why the foreground is pinned to --brand-white rather than left to the theme —
       a fixed fill has to carry its own contrast (5.74:1 in both themes; see the brand table).

       Every neutral here is mixed from --tv-body-fg rather than reusing --tv-surface-alt /
       --tv-color-text-secondary / --tv-color-hover: those dilute *currentColor*, and every
       surface in this form (the tab strip, the float label, and now the header itself) sits
       inside an element the theme has already tinted its own blue — so a currentColor-derived
       "neutral" renders as a translucent blue wash. */
    --tv-form-surface: var(--tv-surface);
    --tv-form-section-header-surface: var(--brand-blue);
    --tv-form-section-header-fg: var(--brand-white);
    /* Hover/expanded darken the band rather than lighten it: lightening BLUE drops the white
       label below AA, darkening only raises it. */
    --tv-form-section-header-hover: color-mix(in srgb, var(--brand-blue) 85%, var(--brand-black));
    --tv-form-hover: color-mix(in srgb, var(--tv-body-fg) 10%, transparent);
    /* Filled well behind read-only fields. Was the same token as the section header until the
       header became a brand fill — which silently painted every disabled input solid blue. */
    --tv-form-well: color-mix(in srgb, var(--tv-body-fg) 6%, transparent);
    --tv-form-field-label: color-mix(in srgb, var(--tv-body-fg) 72%, transparent);
    --tv-form-field-border: var(--tv-border-color);

    /* ── Tier 2: generic indicators (value-vs-target cards) ── */
    --indicator-neutral: currentColor;
    --indicator-good: var(--palette-green);
    --indicator-warning: var(--palette-amber);
    --indicator-bad: var(--palette-red);

    /* ── Tier 2: time-registration approval domain ──
       Distinct from task/invoice status on purpose — the approved green (#4CA44C)
       is intentionally lighter than the generic --palette-green.

       "Employee closed" / "done" is the deliberate exception to the "no brand hues for status"
       rule. It used to sit on --palette-blue, i.e. the theme's *info* cyan (#0dcaf0 in Bootstrap
       5.3) — white text on it measures ~1.7:1 and it read as a stray Bootstrap colour next to the
       brand-blue primary. Done is also the one status painted as a solid button fill (the
       "Færdig" action in the time-registration editor), so it needs a fill that carries white
       text on both themes: --brand-blue does (5.74:1). The row token below follows it so a row
       and its period agree. */
    --approval-draft: var(--palette-amber);
    --approval-employee-closed: var(--brand-blue);
    --approval-management-approved: #4CA44C;
    --approval-management-denied: var(--palette-black);

    /* ── Tier 2: time-registration row status (individual entries) ──
       The per-row counterpart to the --approval-* period states above. These lived in C# as
       System.Drawing.Color.FromArgb triplets in TimeRegStatusColors.cs — a third copy of the
       brand hexes, alongside app.css and the (now deleted) scheduler appointment template, which
       had already drifted to #50AD50/#51B6FF/#FF8850. Done and approved reuse the approval tokens
       so a row and its period agree. */
    --timereg-row-draft: var(--palette-orange);
    --timereg-row-done: var(--approval-employee-closed);
    --timereg-row-approved: var(--approval-management-approved);
    --timereg-row-denied: var(--palette-black);

    /* ── Tier 2: day norm status (fill-remaining-hours day strip) ──
       Solid chips with white text rather than coloured glyphs, so these point at the deep palette
       variants. This is the one place a status colour is used as a fill; everywhere else it is text. */
    --day-norm-met: var(--palette-green-deep);
    --day-norm-short: var(--palette-amber-deep);

    /* ── Tier 2: task status domain (TvTask.Status) ── */
    --task-status-notstarted:  var(--palette-grey);
    --task-status-inprogress:  var(--palette-blue);
    --task-status-completed:   var(--palette-green);
    --task-status-approved:    var(--palette-green);
    --task-status-disapproved: var(--palette-red);

    /* ── Tier 2: deadline status domain (TvTaskDeadline.Status) ── */
    --deadline-status-created:  var(--palette-grey);
    --deadline-status-done:     var(--palette-green);
    --deadline-status-declined: var(--palette-red);

    /* ── Tier 2: email delivery status domain (EmailLog.Status) ──
       Amber (not red) for a spam complaint: the message was delivered, so it is a reputation
       warning rather than a failure to deliver. Grey covers both "not yet known" (Queued) and
       "will never be known" — a Flowmailer row, which no webhook ever updates. */
    --email-status-queued:        var(--palette-grey);
    --email-status-sent:          var(--palette-blue);
    --email-status-delivered:     var(--palette-green);
    --email-status-bounced:       var(--palette-red);
    --email-status-spamcomplaint: var(--palette-amber);
    --email-status-failed:        var(--palette-red);
    --email-status-unknown:       var(--palette-grey);

    /* ── Tier 2: invoice status domain (Invoice.Status) ──
       Draft is orange (work in progress) rather than grey, matching the legacy client's
       orange draft overlay; the rest follow the blue → green progression. */
    --invoice-status-draft:      var(--palette-orange);
    --invoice-status-done:       var(--palette-blue);
    --invoice-status-approved:   var(--palette-green);
    --invoice-status-posted:     var(--palette-blue);
    --invoice-status-dispatched: var(--palette-blue);
    --invoice-status-paid:       var(--palette-green);
    --invoice-status-cancelled:  var(--palette-red);

    /* ── Tier 2: scheduled job results (SchedulableExecutionResultEnum) ──
       "Nothing to do" is grey rather than green: it is a non-event, and colouring it as success
       makes an idle job look as reassuring as one that actually did work. */
    --job-result-ok:            var(--palette-green);
    --job-result-witherrors:    var(--palette-amber);
    --job-result-failed:        var(--palette-red);
    --job-result-stopped:       var(--palette-red);
    --job-result-nowork:        var(--palette-grey);
    --job-result-unknown:       var(--palette-grey);
    --job-result-info:          var(--palette-blue);

    /* ── Tier 2: receivables aging bands (invoicing dashboard) ──
       A severity ramp, not a status: green while nothing is due, then progressively hotter the
       longer a receivable has been outstanding. Amber is reused for the first two overdue bands
       because the palette has no distinct "mild warning" hue. */
    --invoice-aging-not-due:    var(--palette-green);
    --invoice-aging-1-30:       var(--palette-amber);
    --invoice-aging-31-60:      var(--palette-orange);
    --invoice-aging-61-90:      var(--palette-red);
    --invoice-aging-over-90:    var(--palette-black);

    /* ── Tier 2: invoice line shortfall/profit (InvoiceDraftLine.Shortfall) ──
       Billed below the calculated price is a loss, above it a profit; zero stays unstyled. */
    --invoice-shortfall-negative: var(--palette-red);
    --invoice-shortfall-positive: var(--palette-green);

    /* ── Tier 2: invoice key figure trend (InvoiceDetail trend chart) ──
       Series identity, not status: the same metric keeps one colour across every invoice on the
       chart. The margin line takes the theme accent so it reads as a different *kind* of quantity
       (a percentage, on its own axis) rather than as a fourth amount. The current invoice reuses
       the draft colour because that is exactly what it is - a figure still being edited. */
    --invoice-trend-price:        var(--palette-blue);
    --invoice-trend-contribution: var(--palette-green);
    --invoice-trend-shortfall:    var(--palette-amber);
    --invoice-trend-margin:       var(--palette-accent);
    --invoice-trend-current:      var(--palette-orange);

    /* ── Tier 2: operation queue domain (QueuedOperation.Status) ──
       Terminal states reuse the indicator tokens; the two non-terminal states get their own
       so "waiting for a worker" reads differently from "neutral". */
    --operation-queue-waiting: var(--palette-grey);
    --operation-queue-running: var(--palette-blue);

    /* ── Tier 2: activity centre badge ──
       A solid accent fill behind white text. It must not use --indicator-neutral: that token is
       currentColor, which in the app bar is the white icon colour, giving a white pill with white
       text. The failed variant is handled by --indicator-bad in the component's own stylesheet. */
    --activity-center-badge-bg: var(--palette-accent);
    --activity-center-badge-fg: var(--tv-on-accent);

    /* ── Tier 2: DMS document status domain (CorrespondenceListItem.ApprovalStatus) ── */    --doc-status-draft:    var(--palette-orange);
    --doc-status-finished: var(--palette-blue);
    --doc-status-denied:   var(--palette-black);
    --doc-status-approved: var(--palette-green);

    /* ══ Tier 2: enum columns rendered as glyphs on the generic entity lists ══
       Each enum column in EntityConfiguration.ColumnIcons pairs an MDI glyph with one of these
       tokens. Two kinds of group live below and they are coloured on different principles:

       • A *status* has a direction — grey/blue "in progress", green "good", red "bad". Colour
         carries meaning and should be read before the glyph.
       • A *taxonomy* (VAT zone, group type, custom-field type) has no direction. Its colours exist
         only to tell members apart, so they are drawn from the categorical hues and must stay
         mutually distinct rather than imply that one member is better than another.

       Domains are kept separate even when two currently resolve to the same primitive: that is what
       lets one diverge later without silently repainting the other. */

    /* ── Audit trail action (AuditLog.Action) ──
       Read is grey, not blue: it is the only member that changed nothing, and colouring it like an
       edit makes a read-heavy log look far busier than it is. */
    --audit-action-none:          var(--palette-grey);
    --audit-action-created:       var(--palette-green);
    --audit-action-updated:       var(--palette-blue);
    --audit-action-deleted:       var(--palette-red);
    --audit-action-statuschanged: var(--palette-amber);
    --audit-action-read:          var(--palette-grey);

    /* ── Company legal form (CompanyType.TypeCode) — taxonomy ── */
    --company-type-none: var(--palette-grey);
    --company-type-as:   var(--palette-blue);
    --company-type-aps:  var(--palette-teal);
    --company-type-amba: var(--palette-purple);

    /* ── Customer lifecycle (Customer.State) ──
       Lead is amber rather than green: it is a prospect, not yet revenue. Blocked takes red and
       Inactive grey, so "we stopped them" reads differently from "they went quiet". */
    --customer-state-active:   var(--palette-green);
    --customer-state-lead:     var(--palette-amber);
    --customer-state-inactive: var(--palette-grey);
    --customer-state-blocked:  var(--palette-red);

    /* ── Automatic invoice dispatch (Customer + Invoice.AutomaticInvoiceDispatch) ──
       Shared by two entities on purpose: it is one setting, shown in two lists. */
    --invoice-dispatch-none:  var(--palette-grey);
    --invoice-dispatch-email: var(--palette-blue);
    --invoice-dispatch-gln:   var(--palette-teal);

    /* ── VAT zone (Customer.VatZone) — taxonomy ── */
    --vat-zone-domestic:      var(--palette-blue);
    --vat-zone-eu:            var(--palette-purple);
    --vat-zone-international: var(--palette-teal);

    /* ── Custom field data type (CustomFieldDefinition.Type) — taxonomy ──
       Int and Double share the numeric teal: they differ in precision, not in kind, and the glyph
       already says which. */
    --custom-field-type-string:   var(--palette-blue);
    --custom-field-type-int:      var(--palette-teal);
    --custom-field-type-double:   var(--palette-teal);
    --custom-field-type-datetime: var(--palette-purple);
    --custom-field-type-guid:     var(--palette-grey);
    --custom-field-type-picklist: var(--palette-amber);

    /* ── Email provider (EmailLog.Provider) ──
       Unknown is grey and is the honest default: only Postmark reports events back, so provider and
       delivery status have to disagree gracefully. */
    --email-provider-unknown:    var(--palette-grey);
    --email-provider-postmark:   var(--palette-blue);
    --email-provider-flowmailer: var(--palette-teal);

    /* ── Absence type (FlexVacationInfo.Type, TvTask.SpecialTypeDesignator) ──
       One domain, two entities: a task's special-type designator and a flex/vacation row name the
       same six kinds of absence, so they must not drift apart. The "…Sum" members are aggregates of
       the type above them and deliberately reuse its colour — the glyph (sigma) is what says
       "total", the colour says "of what". */
    --absence-type-none:           var(--palette-grey);
    --absence-type-vacation:       var(--palette-blue);
    --absence-type-vacation2:      var(--palette-teal);
    --absence-type-maternityleave: var(--palette-purple);
    --absence-type-lieudays:       var(--palette-amber);
    --absence-type-sickness:       var(--palette-red);

    /* ── Task CAP type (TvTask.CAPType) ──
       An audit-independence classification, so it reads as a permission: green allowed, red
       prohibited. None renders no glyph at all and therefore needs no token. */
    --task-captype-auditing:    var(--palette-green);
    --task-captype-nonauditing: var(--palette-blue);
    --task-captype-prohibited:  var(--palette-red);

    /* ── Sales opportunity rating (TvTask.OpRating) ──
       A funnel, not a status: the ramp runs grey → blue → green as the deal advances, with the two
       terminal states breaking out (accepted green, denied red). InProduction is the accent — it has
       left the funnel entirely and is work now. */
    --task-oprating-contact:        var(--palette-grey);
    --task-oprating-meetingplanned: var(--palette-blue);
    --task-oprating-meetingdone:    var(--palette-blue);
    --task-oprating-agreedonquote:  var(--palette-amber);
    --task-oprating-quotesent:      var(--palette-amber);
    --task-oprating-homestretch:    var(--palette-orange);
    --task-oprating-quoteaccepted:  var(--palette-green);
    --task-oprating-quotedenied:    var(--palette-red);
    --task-oprating-inproduction:   var(--palette-accent);

    /* ── Invoice type (Invoice.InvoiceType) ──
       Two axes in one enum: what is billed (aconto = amber, time registrations = blue) and how the
       invoice came about (the glyph). Colour therefore groups the aconto family together and the
       time-registration family together, which is the distinction that matters when scanning. */
    --invoice-type-normal:                     var(--palette-blue);
    --invoice-type-aconto:                     var(--palette-amber);
    --invoice-type-recurring-template-aconto:  var(--palette-amber);
    --invoice-type-recurring-template-timereg: var(--palette-blue);
    --invoice-type-from-recurring-aconto:      var(--palette-amber);
    --invoice-type-from-recurring-timereg:     var(--palette-blue);
    --invoice-type-from-automatic-timereg:     var(--palette-teal);

    /* ── Invoice line type (InvoiceLine.InvoiceLineType) — taxonomy ──
       Aconto keeps the amber it has on the invoice itself, so a line and its invoice agree. */
    --invoice-line-type-normal:                var(--palette-blue);
    --invoice-line-type-aconto:                var(--palette-amber);
    --invoice-line-type-recurring:             var(--palette-teal);
    --invoice-line-type-recurring-from-task:   var(--palette-purple);

    /* ── Invoice rule granularity (InvoiceRule.Type) — taxonomy ── */
    --invoice-rule-type-per-line:    var(--palette-blue);
    --invoice-rule-type-per-task:    var(--palette-teal);
    --invoice-rule-type-per-invoice: var(--palette-purple);

    /* ── Report kind (ReportDefinition.Type) — taxonomy ── */
    --report-type-none:    var(--palette-grey);
    --report-type-invoice: var(--palette-blue);

    /* ── Standard text kind (StandardText.Type) — taxonomy ── */
    --standard-text-type-invoiceline:  var(--palette-blue);
    --standard-text-type-invoiceemail: var(--palette-teal);

    /* ── Organisational unit kind (TvGroup.GroupType) — taxonomy ──
       Deliberately not a hierarchy ramp: a division is not "better" than a team, and colouring it
       as one invites reading rank into what is only a shape of grouping. */
    --group-type-usergroup:  var(--palette-blue);
    --group-type-department: var(--palette-teal);
    --group-type-team:       var(--palette-green);
    --group-type-division:   var(--palette-purple);
    --group-type-office:     var(--palette-amber);

    /* ── Flex settlement interval (UserContract/UserContractTemplate.FlexInterval) ──
       A duration ramp, not a status: the longer the interval, the further along the hue runs. */
    --flex-interval-weekly:      var(--palette-blue);
    --flex-interval-monthly:     var(--palette-teal);
    --flex-interval-threemonths: var(--palette-green);
    --flex-interval-sixmonths:   var(--palette-amber);
    --flex-interval-twelvemonths: var(--palette-purple);

    /* ── Tier 2: time-reg schema pin affordances (TimeRegSchema) ──
       Not a status domain: these are chrome. --pin-toggle stays a neutral, because it also draws the
       ring on the topbar's *primary-blue* quick-add toggle, where a coloured ring would be
       accent-on-accent and vanish.
       The row glyphs get their own pair. Unpinned is the info blue on the outline glyph: coloured
       enough to read at 18px, but the outline keeps its visual weight below the pinned state.
       Pinned is orange — a warm hue no other schema affordance uses, so a pinned row is spotted
       without reading the icon and never confused with today's blue column or a primary button.
       Orange is an explicit hex rather than --palette-amber because amber inherits the theme's
       warning colour, which on light themes is a pale yellow that all but disappears on white. */
    --pin-toggle: var(--color-sf-content-text-color, #e0e0e0);
    --pin-toggle-pinned: var(--palette-accent);
    --pin-glyph: var(--palette-blue);
    --pin-glyph-pinned: var(--palette-orange);

    /* ── Tier 2: planning grid domain (PlanningGrid) ──
       Restricted-row label/glyph is a muted neutral, not a status color - it signals "hidden by
       design", not good/bad/pending. Rejected/pending/receipt cell accents reuse the generic
       indicator tokens above rather than inventing a parallel red/blue/green. */
    --planning-restricted: var(--palette-grey);
    --planning-inactive-customer: var(--indicator-warning);
    --planning-cell-pending: var(--indicator-neutral);
    --planning-cell-rejected: var(--indicator-bad);
    --planning-cell-receipt: var(--indicator-good);

    /* -- Tier 2: planning capacity scale (PlanningCapacityCell, #575/#576/#577) --
       Five bands diverging around 100 %: below is spare capacity, around 100 % is balance, above is
       overbooked. Both ends are problems and the middle is the healthy one, which is why this is a
       diverging scale and not the sequential --indicator-* good-to-bad set.

       The outer bands reuse the primitives so they track the theme, matching the mockup's own blue
       and red. The two inboard tints are explicit rgba rather than a color-mix() of those primitives,
       for the reason documented on --timereg-today-tint below: color-mix() over var(--color-sf-*)
       resolves to an invalid colour on themes that expose those as bare RGB triplets (Material 3),
       which drops the fill entirely instead of degrading. The channels are the mockup's #d03b3b and
       #256abf at the alpha its light tints imply.

       The balance band is deliberately NOT one of the mockup's near-white/near-black pair. Freezing
       #f0efec here would paint a near-white fill on the dark theme - the exact failure the
       --tv-color-text-secondary comment above records having fixed. Deriving it from currentColor
       tracks every theme, and currentColor-based color-mix() is safe on Material 3 because it reads
       no --color-sf-* variable. */
    --planning-load-over: var(--palette-red);
    --planning-load-over-soft: rgba(208, 59, 59, 0.28);
    --planning-load-mid: var(--tv-surface-alt);
    --planning-load-spare-soft: rgba(38, 106, 191, 0.28);
    --planning-load-spare: var(--palette-blue);

    /* ── Tier 2: time registration schema — today's column ──
       Today is a navigational cue, not a status, so it must not collide with the approval
       colours the schema already paints onto day headers.
       The tint is written as an explicit rgba rather than derived from --palette-blue with
       color-mix()/relative-colour syntax: those resolve to an invalid colour on themes that
       define --color-sf-* as bare RGB triplets (Material 3), which would silently drop the
       highlight entirely rather than degrade. */
    --timereg-today-accent: var(--palette-blue);
    --timereg-today-tint: rgba(77, 173, 255, 0.12);

    /* Drag & drop of hours between days in the schema. The "over" tint is written as an explicit
       rgba rather than a colour-mix of the primitive: it sits behind the cell's textbox, so it has
       to stay translucent no matter which theme resolves --palette-blue. */
    --timereg-drop-valid: var(--palette-blue);
    --timereg-drop-over: rgba(77, 173, 255, 0.22);

    /* ── Tier 2: notifications (toasts + error dialog) ──
       Toasts sit in the bottom-left corner, flush to the edge: they deliberately overlay the
       navigation when it is open, which the stacking scale below permits (toast 1300 > sidebar 1001).
       Colours point at the Tier-1 primitives, which inherit from the active Syncfusion theme, so the
       accent of a toast matches the rest of the app instead of a second hardcoded palette. */
    --tv-toast-offset-block: 1.25rem;
    --tv-toast-offset-inline: 0;

    /* Toast surfaces are mixed from the accent into the *theme's* own toast background, so light,
       dark and high contrast all work from one declaration — no parallel [data-theme] block.
       The mix stays opaque: a translucent toast would show the page through it and wreck contrast. */
    --notification-info-accent: var(--palette-blue);
    --notification-info-surface: color-mix(in srgb, var(--notification-info-accent) 14%, var(--color-sf-content-bg-color, #fff));
    --notification-success-accent: var(--palette-green);
    --notification-success-surface: color-mix(in srgb, var(--notification-success-accent) 14%, var(--color-sf-content-bg-color, #fff));
    --notification-warning-accent: var(--palette-amber);
    --notification-warning-surface: color-mix(in srgb, var(--notification-warning-accent) 16%, var(--color-sf-content-bg-color, #fff));
    --notification-error-accent: var(--palette-red);
    --notification-error-surface: color-mix(in srgb, var(--notification-error-accent) 14%, var(--color-sf-content-bg-color, #fff));

    /* Fremdriftsringen on "Mangler at blive planlagt" (#573). Both are set explicitly because
       Syncfusion writes the circular ring's colours as SVG *presentation attributes*
       (stroke="#E9ECEF" / "#007bff") rather than taking them from the theme stylesheet - so an
       unstyled ring renders a near-white track on the dark theme's #1e1e1e panel, where the empty
       portion is the loudest thing on the row and a 0% ring reads as a full one. */
    --planning-progress-ring: var(--palette-accent);
    --planning-progress-track: var(--color-sf-border, #495057);

    /* ── Tier 2: file-type glyphs (DMS document list) ──
       Vendor brand hues (Word/Excel/PDF) plus generic image/archive accents;
       no Tier-1 semantic match exists for these, so they stay explicit hex. */
    --filetype-word:    #2b579a;
    --filetype-excel:   #217346;
    --filetype-powerpoint: #d24726;
    --filetype-pdf:     #d93025;
    --filetype-email:   #0072c6;
    --filetype-image:   #8e44ad;
    --filetype-archive: #f39c12;

    /* ── Tier 2: inline code and code blocks (AI chat, markdown output) ──
       A code block keeps a dark surface on light themes too — that is the near-universal
       convention for syntax-highlighted output and is deliberate, like the file-type brand hues
       above. Inline code, by contrast, is a wash over the surrounding text and does follow the
       theme. Both were previously hardcoded in theme-overrides.css with a duplicate dark branch. */
    --tv-code-surface: var(--tv-surface-alt);
    --tv-code-block-surface: #1e1e1e;
    --tv-code-block-text: #d4d4d4;

    /* Form validation / error accents. Carried the Blazor template default (#e50000) as a third
       competing red; theme-overrides.css redefined the same name at :root and, loading later,
       always won — so this value never rendered. Single definition, on the palette. */
    --color-danger: var(--palette-red);
    --color-danger-light: color-mix(in srgb, var(--color-danger) 12%, transparent);

    /* ── Stacking order for app chrome ──
       The sticky TopBar sits at 1000 and the main-layout sidebar is lifted to
       1001 (see theme-overrides.css) so it covers the TopBar on mobile. The AI
       chat slide-over must clear the sidebar — expanded, it spans the full
       viewport width. A modal dialog must clear everything, so it is
       deliberately defined one band above both.
       Toasts sit *above* ordinary dialogs, not below: several screens (the mobile time-tracking
       timer, for one) are themselves modal dialogs, and a toast they raise would otherwise render
       behind their own overlay. The error dialog then has to clear the toasts too, so an exception
       is never hidden behind the notification reporting it. */
    --tv-z-topbar: 1000;
    /* The layout sidebar must cover the sticky TopBar on mobile, where its type is Over. */
    --tv-z-sidebar: 1001;
    --tv-z-chat-panel: 1050;
    --tv-z-dialog: 1100;
    /* Full-screen app overlays (mobile global search, planning grid busy shade). */
    --tv-z-overlay: 1200;
    --tv-z-toast: 1300;
    --tv-z-error-dialog: 1400;
}

html, body {
    font-family: var(--tv-font-sans);
}

/* Bootstrap's own stylesheet is not loaded (see App.razor), so these few element
   defaults have to be stated here or links fall back to the browser's default blue.
   They previously carried the stock template's hardcoded light-theme hex values,
   which stayed light-blue on the dark and high-contrast themes; they now track the
   accent token instead. */
a, .btn-link {
    color: var(--tv-accent);
}

.btn-primary {
    color: var(--tv-on-accent);
    background-color: var(--tv-accent);
    border-color: var(--tv-accent);
}

/* The inner white ring was invisible against a dark panel, leaving a single-colour
   halo instead of the intended two-tone focus indicator. currentColor's inverse is
   not available in CSS, so the surface token supplies the separating ring. */
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
    box-shadow: 0 0 0 0.1rem var(--tv-focus-ring-inner), 0 0 0 0.25rem var(--tv-focus-ring);
}

/* The layout markup carried Bootstrap's `px-2` for its horizontal gutter, but the Bootstrap
   stylesheet is not loaded (its <link> is commented out in App.razor), so that class resolved to
   nothing and every page — the full-bleed entity grids most visibly — ran edge-to-edge. The
   gutter is owned here instead, via the shared token. */
.content {
    padding-top: var(--tv-content-top-gap);
    padding-inline: var(--tv-page-gutter);
}

/* Shared wrapper for the list-style pages (entity lists, the DMS lists and editor, global
   search). It deliberately adds no horizontal or top padding: .content above already supplies
   the page gutter, and a second copy here is what pushed the entity list's sticky top bar a
   gutter further right — and a gutter lower — than every other page's, since EntityDetail and
   TimeRegSchema render their top bar straight into .content. Only the trailing gap below the
   grid belongs to this wrapper.

   Defined once here rather than in a per-page <style> block: it was declared identically in
   four of them, so correcting one would silently have left the other three behind. */
.tv-item-list-page {
    padding-block: 0 var(--tv-space-4);
}

/* State borders for summary/KPI cards. These four class names are used across the dashboard, the
   landing page and the organisation hierarchy page, and were documented as an existing mechanism,
   but no stylesheet ever defined them — so every "state border" in the app silently rendered as
   no border at all. Defined here against the Tier-2 indicator tokens, next to the other globals
   the components assume exist.

   Good and Neutral are deliberately borderless: they are the absence of a problem, and marking
   them is what made every card on a page shout equally loudly. The tokens are kept for callers
   that want the colour, but the border is reserved for states that need attention. */
.card-border-warning,
.card-border-bad {
    border-left: 4px solid;
}

.card-border-warning {
    border-left-color: var(--indicator-warning);
}

.card-border-bad {
    border-left-color: var(--indicator-bad);
}

h1:focus {
    outline: none;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid var(--color-danger);
}

.validation-message {
    color: var(--color-danger);
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}

/* Gør knappen til referencepunkt for placering */
.e-btn.timeregistrationSchemaLinkToTimereg {
    position: relative !important;
    overflow: visible !important; /* Vigtigt: så badget ikke bliver klippet af */
}

/* Placer badget helt oppe i højre hjørne */
.custom-inline-dot {
    position: absolute !important;
    top: -5px; /* Dette virker allerede hos dig */
    /* NYE RETTELSER: */
    right: -7px !important; /* Prøv en høj værdi for at se effekten */
    left: auto !important; /* Vigtigt: fjerner eventuel låsning til venstre side */
    width: 13px !important;
    height: 16px !important;
    background-color: lightgray !important;
    display: flex !important;
    justify-content: center;
    align-items: center;
    margin: 0 !important;
    padding: 0 !important; /* Fjern din padding-bottom: 14px hvis muligt */
    color: black !important;
}

/* Row-level status chips in the scheduler. These previously painted themselves from the
   --approval-* *period* tokens while TimeRegStatusColors.cs painted the same statuses from its own
   System.Drawing hexes — so the class rendered amber where the C# constant said orange. Both now
   read the --timereg-row-* group. */
.employeeDraftColor {
    background-color: var(--timereg-row-draft) !important;
    color: var(--tv-on-accent) !important;
}

.timeregDoneNotApprovedColor {
    background-color: var(--timereg-row-done) !important;
    color: var(--tv-on-accent) !important;
}

.timeregManagementApprovedColor {
    background-color: var(--timereg-row-approved) !important;
    color: var(--tv-on-accent) !important;
}

.timeregManagementDeniedColor {
    background-color: var(--timereg-row-denied) !important;
    color: var(--tv-on-accent) !important;
}

.customTextAlignCenter {
    text-align: center !important;
}

.text-red {
    color: red !important;
}

.text-green {
    color: green !important;
}

/* Reset margin added by Syncfusion e-icon-left on icon-only buttons using MDI */
.e-icon-btn.e-btn .e-btn-icon.mdi {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

/* TvButton visual hierarchy. The Secondary variant is an outline: transparent bg, theme-aware
   text/icon/border, primary colour on hover. Primary (.e-primary) and Danger (.e-danger) keep the
   Syncfusion solid fill, so they are excluded here — an unscoped rule with !important silently
   turned every IsPrimary button in the app into an outline. :where() keeps the exclusion at zero
   specificity so the scoped .tv-btn-colored overrides in TvButton.razor.css still win. */
.e-btn.tv-btn:where(:not(.e-primary, .e-danger)) {
    background-color: transparent !important;
    border: 1px solid var(--color-sf-content-text-color) !important;
    color: var(--color-sf-content-text-color) !important;
}

.e-btn.tv-btn .e-btn-icon {
    color: inherit !important;
}

.e-btn.tv-btn:where(:not(.e-primary, .e-danger)):hover,
.e-btn.tv-btn:where(:not(.e-primary, .e-danger)):focus {
    background-color: transparent !important;
    border-color: var(--color-sf-primary) !important;
    color: var(--color-sf-primary) !important;
}

.e-btn.tv-btn:where(:not(.e-primary, .e-danger)):active {
    background-color: color-mix(in srgb, var(--color-sf-primary) 10%, transparent) !important;
    border-color: var(--color-sf-primary) !important;
    color: var(--color-sf-primary) !important;
}

/* Tertiary variant: the outline without its border. */
.e-btn.tv-btn.e-btn-borderless {
    border: none !important;
}

.e-btn.tv-btn.e-btn-borderless:hover,
.e-btn.tv-btn.e-btn-borderless:focus {
    border: none !important;
}

.e-btn.tv-btn.e-btn-borderless:active {
    border: none !important;
}

/* Planning row markers -------------------------------------------------------------------------
   Global rather than component-scoped, because the same markers are rendered by more than one
   component. Blazor CSS isolation rewrites a .razor.css selector to match only its own component, so
   a shared marker needs a copy per component - and PlanRowsGrid never got one for .planning-restricted,
   which left the restricted label in "Min plan" (the view that actually ships) with no styling at all,
   while MyPlanGrid.razor.css carried a copy that matched nothing. One definition here removes both the
   duplication and that class of bug.

   Both markers put their glyph and label beside the name rather than behind it: no single text colour
   clears WCAG AA against every --indicator-* value the themes resolve to, so a coloured fill with a
   label on top cannot be made accessible in both themes at once. */
.planning-restricted {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    color: var(--planning-restricted);
    font-style: italic;
}

.planning-restricted-icon {
    font-size: 1rem;
}

.planning-inactive-customer {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    color: var(--planning-inactive-customer, hotpink);
}

.planning-inactive-customer-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
}

.planning-inactive-customer-icon {
    font-size: 1rem;
}

/* ─────────────────────────────────────────────────────────────────────────────
   Global search (app bar + results panel + /search page)

   Lives here rather than in scoped component CSS because the box and the results
   panel are two components that share one visual surface: Blazor scoped styles do
   not cross a component boundary, so keeping them together avoids a ::deep chain
   whose specificity has to be maintained by hand.
   ───────────────────────────────────────────────────────────────────────────── */

/* Syncfusion's app bar sets `overflow: hidden` on `.e-appbar`, which clips anything taller than
   the 48px bar. That silently swallows the search results panel whole — the box accepts input,
   queries the API and renders the panel, but none of it is ever painted, so the feature reads as
   completely dead. Two class names beat Syncfusion's single-class rule, so no `!important`.
   Applied via an opt-in class rather than to `.e-appbar` globally, so only the bar that hosts an
   overflowing popup gives up its clipping. */
.e-appbar.appbar-allows-overflow {
    overflow: visible;
}

.global-search {
    position: relative;
    display: flex;
    align-items: center;
    /* Takes what it can up to a readable width, then stops: a search box as wide as
       the window makes the app bar look empty and the results panel unreadably wide. */
    flex: 0 1 26rem;
    min-width: 0;
    margin-inline: .5rem;
}

.global-search__field {
    display: flex;
    align-items: center;
    gap: .25rem;
    width: 100%;
    padding: .125rem .5rem;
    border: 1px solid var(--tv-search-field-border);
    border-radius: 1rem;
    background: var(--tv-search-surface);
}

.global-search__field:focus-within {
    border-color: var(--tv-accent);
}

.global-search__icon,
.global-search__shortcut {
    flex: none;
    opacity: .7;
}

.global-search__shortcut {
    font-size: .75rem;
    padding: .0625rem .25rem;
    border: 1px solid var(--tv-search-border);
    border-radius: .25rem;
    white-space: nowrap;
}

.global-search__input {
    flex: 1 1 auto;
    min-width: 0;
    border: none;
    background: transparent;
    color: inherit;
    font: inherit;
    padding: .25rem 0;
}

.global-search__input:focus {
    outline: none;
}

/* The browser's own clear affordance would sit beside ours and do something subtly
   different (it does not tell Blazor the value changed). */
.global-search__input::-webkit-search-cancel-button {
    display: none;
}

.global-search__clear,
.global-search__close,
.global-search-trigger {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2rem;
    min-height: 2rem;
    border: none;
    border-radius: 50%;
    background: transparent;
    color: inherit;
    cursor: pointer;
}

.global-search__clear:hover,
.global-search__close:hover,
.global-search-trigger:hover {
    background: var(--tv-search-hover);
}

/* ── Mobile overlay ── */

.global-search--overlay {
    position: fixed;
    inset: 0 0 auto 0;
    z-index: 1200;
    flex-direction: column;
    align-items: stretch;
    flex: none;
    margin: 0;
    padding: .5rem;
    background: var(--tv-search-surface);
}

.global-search--overlay .global-search__panel {
    position: static;
    width: auto;
    max-height: calc(100vh - 4rem);
    border: none;
    box-shadow: none;
}

/* ── Results panel ── */

.global-search__panel {
    position: absolute;
    top: calc(100% + var(--tv-space-2));
    left: 0;
    width: min(34rem, 90vw);
    max-height: min(30rem, 70vh);
    overflow-y: auto;
    z-index: 1100;
    border: 1px solid var(--tv-search-field-border);
    border-radius: var(--tv-default-border-radius);
    background: var(--tv-search-surface);
    box-shadow: var(--tv-shadow-flyout-hover);
}

/* Group headers stick while their rows scroll past, so a long "Dokumenter" list still says what
   it is when the user is halfway down it. The solid surface matters: without it rows bleed
   through the header. */
.global-search-group__header {
    position: sticky;
    top: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--tv-space-2);
    padding: var(--tv-space-2) var(--tv-space-3) var(--tv-space-1);
    background: var(--tv-search-surface);
}

.global-search-group + .global-search-group .global-search-group__header {
    border-top: 1px solid var(--tv-search-border);
}

.global-search-group__heading {
    font-size: var(--tv-text-meta);
    font-weight: var(--tv-weight-strong);
    line-height: var(--tv-leading-tight);
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--tv-search-muted);
}

.global-search-group__more,
.global-search__all {
    border: none;
    border-radius: 1rem;
    background: transparent;
    color: var(--tv-accent);
    font: inherit;
    font-size: var(--tv-text-meta);
    font-weight: var(--tv-weight-medium);
    line-height: var(--tv-leading-tight);
    cursor: pointer;
    padding: .125rem var(--tv-space-2);
}

.global-search-group__more:hover,
.global-search__all:hover {
    background: color-mix(in srgb, var(--tv-accent) 12%, transparent);
}

.global-search__all {
    display: block;
    width: 100%;
    border-radius: 0;
    text-align: center;
    padding: var(--tv-space-2);
    border-top: 1px solid var(--tv-search-border);
}

.global-search-group__items {
    list-style: none;
    margin: 0;
    padding: 0 var(--tv-space-1) var(--tv-space-1);
}

.global-search-item {
    display: flex;
    align-items: center;
    gap: var(--tv-space-3);
    padding: var(--tv-space-2) var(--tv-space-3);
    border-radius: calc(var(--tv-default-border-radius) - .125rem);
    cursor: pointer;
}

.global-search-item:hover {
    background: var(--tv-search-hover);
}

.global-search-item--active,
.global-search-item--active:hover {
    /* An inset marker rather than an outline: the row is inside a scrolling panel, and an
       outline would be clipped at the top and bottom edges. The tint is accent-derived so the
       keyboard row is distinguishable from a plain mouse hover. */
    background: color-mix(in srgb, var(--tv-accent) 10%, transparent);
    box-shadow: inset .1875rem 0 0 0 var(--tv-accent);
}

/* The type glyph gets a disc so a row has a visual anchor at scan speed; the disc, not the glyph,
   carries the tint so the icon stays legible on both themes. */
.global-search-item__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 1.875rem;
    height: 1.875rem;
    border-radius: 50%;
    background: var(--tv-surface-alt);
    color: var(--tv-accent);
    font-size: 1rem;
}

.global-search-item__text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    flex: 1 1 auto;
}

.global-search-item__title,
.global-search-item__subtitle {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    line-height: var(--tv-leading-tight);
}

.global-search-item__title {
    font-weight: var(--tv-weight-medium);
}

.global-search-item__subtitle {
    margin-top: .125rem;
    font-size: var(--tv-text-meta);
    color: var(--tv-search-muted);
}

/* Trailing metadata as pills, so "Emne" and the matched-in-content glyph look placed rather than
   abandoned at the far edge of the row. */
.global-search-item__status,
.global-search-item__badge {
    display: inline-flex;
    align-items: center;
    flex: none;
    gap: var(--tv-space-1);
    padding: .125rem var(--tv-space-2);
    border: 1px solid var(--tv-search-border);
    border-radius: 1rem;
    background: var(--tv-surface-alt);
    font-size: var(--tv-text-meta);
    line-height: var(--tv-leading-tight);
}

.global-search-item__badge {
    color: var(--tv-accent);
    border-color: transparent;
    background: color-mix(in srgb, var(--tv-accent) 12%, transparent);
}

/* Demoted hits are dimmed rather than removed. The status label beside them says why, so a
   low-ranked result reads as a decision instead of a bug — colour alone is never the signal. */
.global-search-item--demoted .global-search-item__title,
.global-search-item--demoted .global-search-item__icon {
    color: var(--tv-search-muted);
}

/* ── States and notices ── */

.global-search-state,
.global-search-notice {
    display: flex;
    align-items: flex-start;
    gap: var(--tv-space-3);
    padding: var(--tv-space-4) var(--tv-space-3);
    font-size: var(--tv-text-body);
}

.global-search-state > .mdi,
.global-search-notice > .mdi {
    flex: none;
    font-size: 1.25rem;
    line-height: 1;
    color: var(--tv-accent);
}

.global-search-state--error,
.global-search-state--error > .mdi {
    color: var(--indicator-bad);
}

.global-search-state__hint {
    margin-top: var(--tv-space-1);
    font-size: var(--tv-text-meta);
    color: var(--tv-search-muted);
}

.global-search-notice {
    padding-block: var(--tv-space-2);
    border-top: 1px solid var(--tv-search-border);
    background: var(--tv-surface-alt);
    font-size: var(--tv-text-meta);
    color: var(--tv-search-muted);
}

.global-search__sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ── Full-screen page ──
   Everything below is scoped to the page (`--page`) so the dropdown keeps its compact rendering.
   The page fills the content column like every entity list does; the card and band treatment is
   borrowed from EntityDetail so the screens a user moves between look like siblings. */

/* Lives in the TopBar's right zone; the bar owns the layout, this only lets the chips wrap. */
.global-search-page__filters {
    flex-wrap: wrap;
}

.global-search-page .top-bar {
    margin-bottom: var(--tv-space-4);
}

.global-search-chip {
    border: 1px solid var(--tv-search-field-border);
    border-radius: 1rem;
    background: transparent;
    color: inherit;
    font: inherit;
    font-size: var(--tv-text-body);
    padding: var(--tv-space-1) var(--tv-space-3);
    cursor: pointer;
}

.global-search-chip:hover {
    background: var(--tv-search-hover);
}

.global-search-chip--active,
.global-search-chip--active:hover {
    border-color: var(--tv-accent);
    background: var(--tv-accent);
    color: var(--tv-on-accent);
}

/* Each type is a card whose header is the same brand band EntityDetail draws over its form
   sections — one vocabulary for "this is a section" across the app. The band carries its own
   foreground because a brand fill does not track the theme (see app.css `--tv-form-section-*`). */
.global-search-results--page .global-search-group {
    overflow: hidden;
    margin-bottom: var(--tv-space-4);
    border: 1px solid var(--tv-search-border);
    border-radius: var(--tv-default-border-radius);
    background: var(--tv-surface);
}

.global-search-results--page .global-search-group__header {
    position: static;
    justify-content: flex-start;
    padding: var(--tv-space-2) var(--tv-space-4);
    border-top: none;
    background: var(--tv-form-section-header-surface);
    color: var(--tv-form-section-header-fg);
}

.global-search-results--page .global-search-group__heading {
    font-size: var(--tv-text-section);
    text-transform: none;
    letter-spacing: 0;
    color: inherit;
}

/* Outlined in the band's own foreground: an accent or neutral pill would vanish on brand blue. */
.global-search-group__count {
    font-size: var(--tv-text-meta);
    font-weight: var(--tv-weight-strong);
    line-height: 1;
    padding: .125rem .5rem;
    border-radius: 1rem;
    border: 1px solid currentColor;
    opacity: .85;
}

.global-search-results--page .global-search-group__items {
    padding: 0;
}

/* Full-width rows divide with hairlines instead of the popover's floating rounded rows. */
.global-search-results--page .global-search-item {
    padding: var(--tv-space-2) var(--tv-space-4);
    border-radius: 0;
}

.global-search-results--page .global-search-item + .global-search-item {
    border-top: 1px solid var(--tv-search-border);
}

.global-search-results--page .global-search-item__icon {
    width: 2.25rem;
    height: 2.25rem;
    font-size: 1.125rem;
}

.global-search-results--page .global-search-notice,
.global-search-results--page .global-search-state {
    padding: var(--tv-space-3) var(--tv-space-4);
    border: 1px solid var(--tv-search-border);
    border-radius: var(--tv-default-border-radius);
    background: var(--tv-surface);
}

/* The spinner glyph only spins if something tells it to; MDI ships the class, not the motion. */
@keyframes global-search-spin {
    to { transform: rotate(360deg); }
}

.global-search-state .mdi-spin {
    animation: global-search-spin 1s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
    .global-search-state .mdi-spin {
        animation: none;
    }
}
