/* ⚠️ This was Inter until 2026-08-20, and the swap fixed a live typographic bug rather
   than being a style preference: the Inter-Variable.woff2 that shipped is a SUBSET, and
   its cmap contains no č, ć, ž, š or đ. Every Serbian word carrying one -- which is most
   of this UI -- was silently falling back to a system face, mid-sentence, in a product
   that is entirely in Serbian. Verified in production by measuring glyph widths, not by
   reading the file: "š" rendered at exactly the monospace width.

   Sora-Variable covers the full sr-Latn set (378 glyphs), was already hosted and already
   used by the two static marketing pages, and its wght axis is 100-800 -- the heaviest
   weight anywhere in this product is 800, so nothing is synthesised.

   Inter was DELETED rather than kept as a fallback: keeping it would ship 73 KB that can
   only ever render the letters Sora already has. Sora lacks a few symbols (≈ ✓ → ₂), and
   those fall through to the system stack below exactly as they already did.

   No CDN fallback on purpose (CSP font policy + the no-external-fonts rule), so this file
   is the only source there is. The Dockerfile fails the build if it goes missing. */
@font-face {
    font-family: 'Sora';
    src: url('/fonts/Sora-Variable.woff2') format('woff2');
    font-weight: 100 800;
    font-style: normal;
    font-display: swap;
}

/* Eliminate the iOS Safari ~300ms tap delay on every interactive element.
   ⚠️ This is now the ONLY thing removing it. The viewport meta in App.razor used to carry
   maximum-scale=1.0, user-scalable=no, which also blocked pinch-zoom (WCAG 1.4.4; audit
   2026-10-15 WIZ-04/APP-24) and was removed. touch-action: manipulation disables only the
   double-tap-to-zoom heuristic on these elements, so the tap is immediate and pinch-zoom
   still works. iOS focus-zoom is prevented at its cause instead: inputs are 16px on phones
   (see the <600px block). Do not put the zoom lock back to "fix" either. */
button,
[role="button"],
.bw-emp-card,
.bw-date-card,
.bw-time-slot,
.bw-service-card,
.bw-cycle-pill,
a {
    touch-action: manipulation;
}

/* ============================================================
   Escalybook Design System
   Inspired by Cal.com, Linear, and Vercel dashboards
   ============================================================ */

/* === VARIABLES === */

/* ── CSS Custom Properties ── */
/* THEME-CONTRACT: this :root block holds the LIGHT values of every design token.
   The dark values live in the single `:root[data-theme="dark"]` block further down
   (search "THEME-CONTRACT: DARK"). The attribute is written only by /js/theme-boot.js.
   src/Escalybook.Core.Ui/Services/ThemeService.cs mirrors the dark neutrals into
   MudBlazor's PaletteDark by hand — keep the two in sync.
   This file deliberately contains ZERO OS-preference media queries: users who never
   opted in must stay light. The string itself is banned so a grep guard reads 0. */
:root {
    color-scheme: light;
    --primary: #4B3F72;
    --primary-hover: #3d3360;
    --primary-light: #ede9f5;
    --primary-lighten: #8a7cb8;
    --primary-tint: #f5f3f9;
    --primary-btn-text: #ffffff;
    --secondary: #2AB5A0;
    --secondary-hover: #229e8c;
    --surface: #ffffff;
    --surface-raised: #ffffff;
    --surface-overlay: #ffffff;
    --surface-hover: rgba(0, 0, 0, 0.03);
    --surface-glass: rgba(255, 255, 255, 0.7);
    --surface-glass-strong: rgba(255, 255, 255, 0.85);
    --overlay-scrim: rgba(26, 26, 46, 0.5);
    --overlay-scrim-strong: rgba(10, 8, 16, 0.9);
    --bg: #F7F6F3;
    --bg-warm: #f5f4f0;
    --bg-subtle: #eeede9;
    --dark-base: #1A1A2E;
    /* ONE hue bias for the whole neutral ramp, and it is violet-warm, because the ground
       (--bg #F7F6F3, --bg-warm) and the primary (#4B3F72) both are. The ink used to be
       #1A1A2E, a NAVY, on a warm paper — a small mismatch, and exactly what makes a palette
       read as assembled rather than chosen. Contrast is unchanged for practical purposes:
       15.60 on --bg and 16.86 on --surface, against 15.78 / 17.06 before. */
    --text: #1E1B2A;
    --text-secondary: #4a4a5a;
    --text-muted: #5c5c6c;
    --text-inverse: #ffffff;
    --border: #e0ddd8;
    --border-light: #eeedea;
    --success: #2AB5A0;
    --success-light: #e8f8f5;
    /* INK ON A FILLED --success / --warning (audit 2026-10-15 APP-02). White on these fills is
       2.56:1 and 2.10:1; this ink measures 7.11 and 8.63. The same constant ThemeService's
       PaletteLight derives through ThemeColorMath.ContrastOn for Mud's filled Success/Warning,
       so a Mud chip and a hand-styled chip on the same fill carry the same ink. */
    --success-contrast: #17141F;
    --warning-contrast: #17141F;
    /* ALERT INK, measured (D25). These are the *-text tokens the MudBlazor text-variant
       alerts now bind to, and a text alert has a transparent background — so the ink sits on
       the PAGE background, which on /book/{slug} is the tenant's own colour. Measured against
       the sweep's real tenant bg rgb(205,196,174): the previous values scored 3.15 (success),
       3.73 (error) and 4.09 (warning), all under AA. Darkened to clear 4.5:1 there while
       staying well clear on white (9.5-10.1:1). Recompute both if these ever change. */
    --success-text: #0b4a45;   /* 5.82 on tenant bg, 10.10 on white (was #0f766e, 3.15) */
    --warning: #e6a817;
    --warning-text: #7c2d12;   /* 5.40 on tenant bg,  9.37 on white (was #92400e, 4.09) */
    --warning-light: #fdf6e3;
    /* THEME-CONTRACT: the same constant as ThemeService PaletteLight.Error. One step darker than
       the old #d94f4f (audit 2026-10-15 APP-02): white on it measured 4.05:1, under AA on every
       filled destructive confirm button and error badge. Now 5.17:1 for white on the fill, and
       about 5.2:1 as text on white. Change both sides together. */
    --error: #c53b3b;
    --error-light: #fdf0f0;
    --error-text: #7f1d1d;     /* 5.77 on tenant bg, 10.02 on white (was #b91c1c, 3.73) */
    --info: var(--primary);
    --info-light: var(--primary-light);
    /* --info-text is NOT var(--primary), and must not become it: BookingWizard.razor
       redefines --primary in LIGHT mode to the tenant's raw unvetted PrimaryColorHex with no
       contrast floor (only the dark path launders it through ThemeColorMath.DarkAccent). A
       pastel tenant hex would then become alert ink on /book/{slug} — a regression from
       today's fixed MudBlazor blue. A concrete value, measured like the rest. */
    --info-text: #3B2F62;      /* 6.84 on tenant bg, 11.87 on white */
    /* Brand surfaces — MODE-INVARIANT (never flip with the theme). A large fill must
       never use --primary, which becomes a pale lavender in dark mode. */
    --brand-gradient: linear-gradient(135deg, #4B3F72 0%, #6B5DB8 55%, #8674CE 100%);
    --brand-ink: #4B3F72;
    --brand-ink-hover: #3d3360;
    --text-on-brand: #ffffff;
    --text-on-brand-secondary: rgba(255, 255, 255, 0.78);
    --glass-on-brand: rgba(255, 255, 255, 0.14);
    /* The quiet zone behind a printed code — the NBS IPS QR on the gost's deposit slip
       (Nekretnine decision 6). MODE-INVARIANT, and not because it looks better: the reader is
       a phone CAMERA, and a dark-mode ground inverts the modules, which many scanners refuse.
       A surface token would have flipped with the theme and broken the payment at night. */
    --qr-paper: #ffffff;
    /* Neutral status pill.
       ⚠️ #64748b was not merely the one cool slate in a violet-warm ramp — it MEASURED BELOW
       AA. And the first replacement, #6E6880, ALSO measured below AA, because the first
       measurement was taken against the wrong thing.

       THE TEXT DOES NOT SIT ON THE GROUND. It sits on the pill, and the pill is a 10% tint of
       the same hue composited over the ground — which LIGHTENS what the ink sits on. Measuring
       ink-against-ground overstated the ratio by 0.4-0.6 and reported 4.53-5.31 for a value
       whose real range is 4.03-4.67. Caught by the Codex gate on blazor#429; the guard now
       composites before it measures.

       Measured on the composited pill, worst ground first:
         --bg-subtle  5.27    --bg-warm  5.59    --bg  5.69    --surface  6.11 */
    --pill-neutral-bg: rgba(110, 104, 128, 0.1);
    --pill-neutral-text: #5C5670;
    /* Decorative accents */
    --accent-indigo: #6366f1;
    --accent-violet: #8b5cf6;
    /* Categorical slots for the ServicesList accent ramp. Their light values are the exact
       legacy hexes so existing tenants' service tiles do not change colour in light mode —
       folding them onto --accent-violet/--accent-indigo shifted two of eight slots. */
    --accent-plum: #6b5b9a;
    --accent-pink: #ec4899;
    --special-accent: #7C5CBF;
    --special-accent-bg: #f1ebfb;
    /* Feature-specific invariants */
    --brand-whatsapp: #25D366;
    --chat-bubble-out: #dcf8c6;
    --chat-bubble-in: #ffffff;
    --chat-unread: #2e7d32;
    --rating-gold: #d4af37;
    /* Calendar category palette (WeeklyCalendar) */
    --cat-1: #4B8BBE;
    --cat-2: #6BA368;
    --cat-3: #E88D4F;
    --cat-4: #9B72CF;
    --cat-5: #E06C75;
    --cat-6: #56B6C2;
    --cat-7: #D4A04E;
    --cat-8: #C678DD;
    --cat-label: #ffffff;
    --cal-series-l: 55%;
    --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.04);
    --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.06), 0 2px 4px -2px rgba(0, 0, 0, 0.04);
    --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.06), 0 4px 6px -4px rgba(0, 0, 0, 0.04);
    --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.06), 0 8px 10px -6px rgba(0, 0, 0, 0.04);
    --shadow-primary: 0 4px 14px rgba(75, 63, 114, 0.35);
    /* ELEVATION POLICY. A resting surface gets a 1px hairline and NO shadow; shadow is spent
       only on something that genuinely floats above the page. This is the one thing every
       premium reference in the 2026-08 Mobbin sweep agreed on (Linear, Vercel, Attio, Midday,
       Stripe) and the one the product most contradicted — 70 box-shadow sites, most of them
       on cards and rows that never leave the page. Reach for these three, not for the raw
       --shadow-* scale, which stays only for the callers that already tuned a specific value. */
    --elev-flat: none;              /* cards, list rows, panels, tiles, table containers   */
    --elev-float: var(--shadow-md); /* menus, popovers, dialogs, toasts, the FAB           */
    --elev-sticky: var(--shadow-xs);/* a sticky bar with content scrolled underneath it    */
    /* APP-13: controls use sm; resting cards/panels use md. lg remains for floating
       dialogs, sheet corners and existing silhouettes. Circles keep 50%, pills full.
       Migration scope and deliberate exceptions: docs/agents/core-ui/scale-tokens.md. */
    --radius-sm: 8px;
    --radius-md: 12px;
    --radius-lg: 16px;
    /* ⚠️ MARKETING ONLY, and as of 2026-09-08 it has NO CALLERS AT ALL. Every one of them
       was a .home-* hero block in the Premium Landing section, which went with Pages/Home.razor
       — "/" is served as StaticLanding/landing.html, which carries its own tokens. The scale
       entry is kept so the ladder is not left with a hole, not because something needs it.
       A 20px radius on a data surface reads as a consumer app, not a tool: the sweep caps a
       card at 12px and a control at 8px, so new work on /dashboard/*, /admin/* or /book/*
       uses --radius-md or smaller. Reaching for this one needs a reason. */
    --radius-xl: 20px;
    --radius-full: 9999px;
    --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
    --transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);
    --transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
    /* ── Motion tokens (PWA foundation, decision 3 / step 1) ──
       Durations and curves as SEPARATE tokens, unlike the three --transition-* shorthands above,
       which bake one curve into one duration and therefore cannot express "this distance, that
       curve". The old three stay exactly as they are — ~180 declarations in this file already
       depend on them and none of them is being retimed here. New motion uses the pair.
       ⚠️ A duration token is only honest if the reduced-motion opt-out reaches it: the
       `prefers-reduced-motion` block near the end of this file zeroes every one of the classes
       built on these, on top of the global animation/transition damper already at ~line 2996. */
    --motion-fast: 150ms;
    --motion-base: 220ms;
    --motion-slow: 320ms;
    /* Standard = enters and leaves under the user's finger; decelerate = something arriving
       (a bar, a sheet); accelerate = something leaving the screen for good. */
    --ease-standard: cubic-bezier(0.2, 0, 0, 1);
    --ease-decelerate: cubic-bezier(0, 0, 0.2, 1);
    --ease-accelerate: cubic-bezier(0.4, 0, 1, 1);
    /* Skeleton shimmer. A dedicated PAIR rather than a reuse of --bg-subtle/--surface: in dark
       mode --surface (#1E1C27) is DARKER than --bg-subtle (#262332), so the obvious reuse
       inverts the sweep and the shimmer reads as a shadow crossing the placeholder. */
    --skeleton-base: #eeede9;
    --skeleton-shine: #f8f7f5;
    --spacing-unit: 8px;
    /* Spacing scale: 4px steps through 24px, then 32/40/48/64px.
       Tokens are px, so replacing a matching margin/padding/gap is exact even at the
       phone's 14px root. Keep optical 2/3/6px gaps, 10/14/18px compact insets, negative
       offsets, safe-area/calc expressions and runtime calendar geometry when needed. */
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 20px;
    --space-6: 24px;
    --space-8: 32px;
    --space-10: 40px;
    --space-12: 48px;
    --space-16: 64px;
    /* Fixed rem type scale: xs caption, sm secondary/compact label, base ordinary UI,
       md body, lg subheading, xl/2xl/3xl headings. Prefer an exact matching token;
       nearby app labels converge on these steps. Calendar microtype, miniature previews,
       px icon glyphs and the 16px phone-input zoom floor are deliberate exceptions.
       Do not convert px to rem: html/body is 14px on phones, 15px otherwise. */
    --text-xs: 0.75rem;
    --text-sm: 0.8125rem;
    --text-base: 0.875rem;
    --text-md: 1rem;
    --text-lg: 1.125rem;
    --text-xl: 1.25rem;
    --text-2xl: 1.375rem;
    --text-3xl: 1.5rem;
    /* THE DISPLAY TIER, and the reason it exists. The scale used to stop at 24px, so no screen
       had a size that answered the question the screen is for — which is the perceptual
       signature of "competent and plain". Every premium reference sets ONE number or word at
       28-34px and everything else at 12-14px (Mercury's balance, Midday's total, Fresha's day
       count). RULES: at most ONE element per screen may use these, and it is the thing the
       screen exists to tell you; they are set at weight 500, never 700 — a 34px bold numeral
       reads as a template; and they are plain elements with a class, NEVER MudText, because
       ThemeService's THEME-CONTRACT deliberately does not alias MudBlazor's Typo.* to these
       tokens and a Typo.h4 would silently disagree. */
    --text-4xl: 1.75rem;    /* 28px - the answer figure on a card                */
    --text-5xl: 2.125rem;   /* 34px - the single hero figure on a page           */
    /* Touch targets */
    --touch-target-min: 44px;
    /* Status colors */
    /* UI audit 2026-10-15, APP-14 (operator decision): Confirmed is the BRAND PURPLE everywhere.
       The blue #3b82f6 that lived here made the same termin purple on the calendar and blue in the
       klijent's visit history. THEME-CONTRACT: --status-confirmed mirrors --primary (#4B3F72 light,
       #9A89D2 dark) by hand, because a status token must resolve to a value on its own; a brand
       change moves both. The C# side never reads these: StatusHelpers.GetStatusFill/Tint/Ink is
       the one map, and these tokens serve only the .status-chip--* classes and plain CSS users. */
    --status-confirmed: #4B3F72;
    --status-confirmed-bg: rgba(75, 63, 114, 0.1);
    --status-completed: #16a34a;
    --status-completed-bg: rgba(22, 163, 74, 0.1);
    --status-canceled: #ef4444;
    --status-canceled-bg: rgba(239, 68, 68, 0.1);
    --status-noshow: #f59e0b;
    --status-noshow-bg: rgba(245, 158, 11, 0.1);
    --status-rescheduled: #8b5cf6;
    --status-rescheduled-bg: rgba(139, 92, 246, 0.1);
    /* Text-grade inks for the status chips (APP-14): the hues above, used as text on their own
       tint, measured 1.85-3.74:1 in light. These pass AA on the tint and on every ground.
       Confirmed's ink is the primary itself, which already clears AA on its tint. */
    --status-confirmed-text: #4B3F72;
    --status-completed-text: #166534;
    --status-canceled-text: #b91c1c;
    --status-noshow-text: #92400e;
    --status-rescheduled-text: #6d28d9;
    /* ⚠️ THE SAME SLATE, IN A SECOND PLACE. --pill-neutral-* was corrected and this pair was
       left on #64748b — .status-chip--default and the treatment-plan controls then rendered at
       3.63-4.21 on the composited pill, under AA on all four grounds. The first guard only
       measured --pill-neutral-*, so it was green while this failed. (Codex gate, blazor#429.)
       Measured on the composited pill: 5.18 subtle / 5.51 warm / 5.60 bg / 6.02 surface. */
    --status-pending: #5C5670;
    --status-pending-bg: rgba(92, 86, 112, 0.1);
    /* Dental chart (src/Escalybook.Core.Ui: ToothSvg, DentalArch).
       Two independent channels, and the split is the whole point: HUE says WHAT was found —
       red caries, blue restoration, gold crown/bridge, steel implant, grey missing — and the
       FILL says whether it is done or only planned. They live here, not in the RCL, because
       core.ui ships no stylesheet and a literal written into a component paints a light-mode
       colour onto a dark page (R4/R5).
       Flat since 2026-10-30 (operator decision): one enamel fill, one outline stroke, one
       groove stroke, no lighting layer and no gum. The values are the approved mockup's. */
    /* Enamel is a warm off-white, not paper white: the chart sits on a warm card and a pure
       white crown read as a cut-out. */
    --tsvg-enamel: #FCFAF4;
    /* The crown edge, and the fissures: lighter and thinner than the edge, so they read as
       grooves in the enamel and not as a second outline. */
    --tsvg-line: #9C95AD;
    --tsvg-groove: #C2BACE;
    --tsvg-caries: #d64545;
    --tsvg-filling: #3e7cb1;
    --tsvg-crown: #c9922b;
    --tsvg-implant: #6e7b8b;
    --tsvg-missing: #b9b4c2;
    --tsvg-other: #7a6ca8;
    /* Treatment plan arch (variant B, 2026-10-30): the SAME teeth, tinted by what the plan says
       about them rather than by findings. Planned is a warm amber ground with a dark amber edge,
       done a green ground with a green edge, rejected a grey ground with a grey edge. The ink
       values are the plan's own three statuses in the chip palette, so the arch, the legend chips
       and the row chips agree. */
    --tpb-planned: #FBEFD6;
    --tpb-planned-line: #C25E0E;
    --tpb-done: #E3F3EA;
    --tpb-done-line: #1F6B45;
    --tpb-rejected: #EEECF1;
    --tpb-rejected-line: #8E889E;
    /* Table */
    --table-header-bg: #fafbfc;
}

/* ============================================================
   THEME-CONTRACT: DARK
   The one and only dark override block. It re-declares the SAME custom-property
   names — never per-element dark restyling, never an OS-preference media query.
   The `data-theme` attribute is written exclusively by /js/theme-boot.js
   (storage key 'bookly_theme', values light|dark|system; absent/invalid → light).
   MudBlazor is synchronized by hand in ThemeService.BuildTheme's PaletteDark.
   ============================================================ */
:root[data-theme="dark"] {
    color-scheme: dark;
    --primary: #9A89D2;
    --primary-hover: #AC9EDF;
    --primary-light: rgba(154, 137, 210, 0.16);
    /* THEME-CONTRACT: exactly what ThemeColorMath.DarkAccent("#4B3F72", targetL: 0.76)
       returns, so MudBlazor's --mud-palette-primary-lighten and this token render the
       same shade. Recompute if the default primary ever changes. */
    --primary-lighten: #B3A6DD;
    --primary-tint: rgba(154, 137, 210, 0.09);
    --primary-btn-text: #17141F;
    --secondary: #2AB5A0;
    --secondary-hover: #35C9B3;
    --surface: #1E1C27;
    --surface-raised: #282436;
    --surface-overlay: #343041;
    --surface-hover: rgba(255, 255, 255, 0.05);
    --surface-glass: rgba(30, 28, 39, 0.72);
    --surface-glass-strong: rgba(30, 28, 39, 0.9);
    --overlay-scrim: rgba(8, 6, 14, 0.65);
    --overlay-scrim-strong: rgba(10, 8, 16, 0.9);
    --bg: #15131B;
    --bg-warm: #17151E;
    --bg-subtle: #262332;
    --dark-base: #1A1A2E;
    --text: #E7E5F2;
    --text-secondary: #B7B4C7;
    --text-muted: #908DA3;
    --text-inverse: #15131B;
    --border: rgba(255, 255, 255, 0.14);
    --border-light: rgba(255, 255, 255, 0.08);
    --success: #3CC9B4;
    --success-light: rgba(60, 201, 180, 0.15);
    /* The dark fills are light, so the ink is the dark button ink, read from it rather than
       pasted so the two cannot drift (app token to app token, never --mud-palette-*). */
    --success-contrast: var(--primary-btn-text);
    --warning-contrast: var(--primary-btn-text);
    --success-text: #7FE3D2;
    --warning: #EDB53C;
    --warning-text: #F2C464;
    --warning-light: rgba(237, 181, 60, 0.15);
    --error: #EF6C6C;
    --error-light: rgba(239, 108, 108, 0.15);
    --error-text: #F49B9B;
    --info: var(--primary);
    --info-light: var(--primary-light);
    /* THEME-CONTRACT: the same constant as --primary-lighten above — both are
       ThemeColorMath.DarkAccent("#4B3F72", targetL: 0.76). Reused, not re-derived, so the two
       cannot drift. 8.14:1 on --bg. The other dark alert inks already clear AA comfortably
       (warning 11.12, error 8.66, success 11.95), so only this one is new. */
    --info-text: #B3A6DD;
    /* D27 — ink for text sitting ON a --primary-light tint rather than on --bg. --primary
       itself measures 4.80:1 over one tint and only 3.74:1 where two stack (an .eyebrow inside
       a tinted card), and 4.33:1 on the dashboard's active nav link — the primary navigation of
       a surface that is now server-rendered and used all day. Composited properly this measures
       5.12:1 on the double tint. Same THEME-CONTRACT constant as --primary-lighten and
       --info-text above; reused, never re-derived. DARK ONLY — every consumer writes
       var(--primary-on-tint, var(--primary)), so light mode keeps exactly what it has today. */
    --primary-on-tint: #B3A6DD;
    /* Skeleton shimmer, dark half. Base sits one step above --bg so a placeholder reads as a
       surface rather than a hole; shine is base lightened, never --surface (see the light block). */
    --skeleton-base: #262332;
    --skeleton-shine: #33303F;
    /* Brand surfaces stay INVARIANT — repeated here only so the contract is auditable
       in one place; the values are identical to the light block on purpose. */
    --brand-gradient: linear-gradient(135deg, #4B3F72 0%, #6B5DB8 55%, #8674CE 100%);
    --brand-ink: #4B3F72;
    --brand-ink-hover: #3d3360;
    --text-on-brand: #ffffff;
    --text-on-brand-secondary: rgba(255, 255, 255, 0.78);
    --glass-on-brand: rgba(255, 255, 255, 0.14);
    /* Repeated here so the contract is auditable in one place: the QR quiet zone is WHITE in
       dark mode too, on purpose. See the light block for why. */
    --qr-paper: #ffffff;
    /* Neutral status pill — the dark half of the same hue correction. Contrast was never the
       problem here (7.20-8.04 across the dark grounds); the slate hue was. */
    --pill-neutral-bg: rgba(176, 169, 192, 0.16);
    --pill-neutral-text: #B0A9C0;
    /* Decorative accents */
    --accent-indigo: #8B8DF5;
    --accent-violet: #A78BFA;
    /* Lightened one step, and kept clear of --primary's dark #9A89D2 so two categorical
       slots never collapse to the same swatch. */
    --accent-plum: #B0A3D8;
    --accent-pink: #F472B6;
    --special-accent: #A88FE3;
    --special-accent-bg: rgba(168, 143, 227, 0.15);
    /* Feature-specific invariants / dark counterparts */
    --brand-whatsapp: #25D366;
    --chat-bubble-out: #24422F;
    --chat-bubble-in: #282436;
    --chat-unread: #2e7d32;
    --rating-gold: #E3C55C;
    /* Calendar category palette (WeeklyCalendar) — lightened for dark surfaces */
    --cat-1: #6FA6D4;
    --cat-2: #86BC83;
    --cat-3: #EFA76F;
    --cat-4: #B592E0;
    --cat-5: #EA8A91;
    --cat-6: #7BCAD4;
    --cat-7: #E2BA6E;
    --cat-8: #D494E6;
    --cat-label: #17141F;
    --cal-series-l: 68%;
    /* Elevation on dark = surface step + translucent white ring, never a black
       drop shadow (invisible against a dark page). */
    --shadow-xs: 0 0 0 1px rgba(255, 255, 255, 0.06);
    --shadow-sm: 0 0 0 1px rgba(255, 255, 255, 0.07);
    --shadow-md: 0 0 0 1px rgba(255, 255, 255, 0.08), 0 4px 12px rgba(0, 0, 0, 0.35);
    --shadow-lg: 0 0 0 1px rgba(255, 255, 255, 0.10), 0 12px 28px rgba(0, 0, 0, 0.45);
    --shadow-xl: 0 0 0 1px rgba(255, 255, 255, 0.12), 0 24px 48px rgba(0, 0, 0, 0.55);
    --shadow-primary: 0 4px 14px rgba(0, 0, 0, 0.5);
    /* Status colors — solids unchanged except rescheduled/pending; tint alphas 0.1 → 0.18 */
    /* THEME-CONTRACT: the dark --primary (APP-14, Confirmed is brand purple). */
    --status-confirmed: #9A89D2;
    --status-confirmed-bg: rgba(154, 137, 210, 0.18);
    --status-completed: #16a34a;
    --status-completed-bg: rgba(22, 163, 74, 0.18);
    --status-canceled: #ef4444;
    --status-canceled-bg: rgba(239, 68, 68, 0.18);
    --status-noshow: #f59e0b;
    --status-noshow-bg: rgba(245, 158, 11, 0.18);
    --status-rescheduled: #A78BFA;
    --status-rescheduled-bg: rgba(167, 139, 250, 0.18);
    /* Status chip inks, dark (APP-14). Confirmed mirrors --primary-on-tint; noshow mirrors
       --warning-text; rescheduled is its own base, which already reads on the dark tint. */
    --status-confirmed-text: #B3A6DD;
    --status-completed-text: #4ADE80;
    --status-canceled-text: #F48A8A;
    --status-noshow-text: #F2C464;
    --status-rescheduled-text: #A78BFA;
    /* The dark half of the same correction. #94A3B8 measured 4.27 on --surface-raised and
       4.35 on --bg-subtle — also under AA, and also invisible to the first guard.
       Now 5.00 at worst across the five dark grounds. */
    --status-pending: #B8B0C8;
    --status-pending-bg: rgba(184, 176, 200, 0.18);
    /* Dental chart — same roles, lifted for a dark ground. The enamel is a dark surface, not
       white: a chart of sound teeth is mostly enamel, and leaving it white would put a sheet of
       paper in the middle of a dark page. The outline is the light ink, the groove a step below
       it, so both still read against the enamel (edge 2.6:1, groove 1.7:1 on the enamel). */
    --tsvg-enamel: #4A4560;
    --tsvg-line: #B5AECB;
    --tsvg-groove: #8A82A3;
    --tsvg-caries: #e06c6c;
    --tsvg-filling: #6ba3d6;
    --tsvg-crown: #e0b054;
    --tsvg-implant: #93a1b3;
    --tsvg-missing: #6a6478;
    --tsvg-other: #a697d6;
    /* Treatment plan arch on a dark ground: the tints are dark versions of the same three hues,
       the edges the lifted inks the status chips already use in dark mode. */
    --tpb-planned: #4A3D2A;
    --tpb-planned-line: #E89A52;
    --tpb-done: #2B4636;
    --tpb-done-line: #6FCF97;
    --tpb-rejected: #3B3846;
    --tpb-rejected-line: #A49DB5;
    /* Table */
    --table-header-bg: #232030;
}

/* ── The theme switch snaps (UI audit 2026-10-15, MOT-14) ──
   The data-theme-switching attribute is written by theme-boot.js (package D, #698), which
   brackets the data-theme write with it for two frames. Without it every control with a colour
   transition (buttons, chips, nav links, slots) faded for 200 ms while the ground, the text and
   the cards changed at once, so light chips lingered on a dark page. The attribute is a flag, not a theme: it never selects
   a colour. */
:root[data-theme-switching] *,
:root[data-theme-switching] *::before,
:root[data-theme-switching] *::after {
    transition: none !important;
}

/* ── Mode-invariant utility classes ──
   These deliberately carry raw literals: they are the same in both themes.
   app.css is the ONE sanctioned home for such values (see R4/R8). */
.impersonation-banner {
    background: #92400e;
    color: #fff;
}

.medal-gold {
    background: linear-gradient(135deg, #fbbf24, #f59e0b);
}

.medal-silver {
    background: linear-gradient(135deg, #9ca3af, #6b7280);
}

.medal-bronze {
    background: linear-gradient(135deg, #d97706, #b45309);
}

/* === ACCESSIBILITY === */

/* Muted text on the dark overlay surface (menus, popovers, dialogs, toast): --text-muted #908DA3 is
   3.97:1 on --surface-overlay #343041. Redefine it INSIDE those surfaces only to #A5A2B8 (5.14:1 on
   #343041, 6.06 on --surface-raised); --text-muted is unchanged everywhere else (5.72 on --bg). */
:root[data-theme="dark"] .mud-popover,
:root[data-theme="dark"] .mud-dialog,
:root[data-theme="dark"] .mud-menu-list,
:root[data-theme="dark"] #blazor-error-ui {
    --text-muted: #A5A2B8;
}

/* Native <button> that replaces a click-only div/span/img: same look, real keyboard semantics. */
.bk-btn-reset {
    appearance: none;
    background: none;
    border: 0;
    margin: 0;
    padding: 0;
    font: inherit;
    color: inherit;
    text-align: inherit;
    cursor: pointer;
}

/* ── Accessibility: Focus-visible styles ── */
*:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
    border-radius: 4px;
}

button:focus-visible,
a:focus-visible,
[tabindex]:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* === BASE & RESET === */

/* ── Base Reset & Typography ── */
*,
*::before,
*::after {
    box-sizing: border-box;
}

html {
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
    scroll-behavior: smooth;
}

html, body {
    font-family: 'Sora', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    font-size: 15px;
    line-height: 1.6;
    color: var(--text);
    background-color: var(--bg);
    margin: 0;
    padding: 0;
    min-height: 100vh;
    min-height: 100dvh;
    -webkit-tap-highlight-color: transparent;
    overscroll-behavior: none;
}

h1, h2, h3, h4, h5, h6 {
    letter-spacing: -0.025em;
    font-weight: 700;
    line-height: 1.3;
    margin: 0;
}

h1 { font-size: 2.25rem; }
h2 { font-size: 1.875rem; }
h3 { font-size: var(--text-3xl); font-weight: 600; letter-spacing: -0.02em; }
h4 { font-size: var(--text-xl); font-weight: 600; letter-spacing: normal; }
h5 { font-size: var(--text-lg); font-weight: 600; letter-spacing: normal; }
h6 { font-size: var(--text-md); font-weight: 600; letter-spacing: normal; }

p {
    color: var(--text-secondary);
    line-height: 1.6;
}

a {
    color: var(--primary);
    text-decoration: none;
    transition: color var(--transition-fast);
}

a:hover {
    color: var(--primary-hover);
}

/* ── Selection Color ── */
::selection {
    background-color: var(--primary-light);
    color: var(--primary-hover);
}

::-moz-selection {
    background-color: var(--primary-light);
    color: var(--primary-hover);
}

/* ── Custom Scrollbar ── */
::-webkit-scrollbar {
    width: 6px;
    height: 6px;
}

::-webkit-scrollbar-track {
    background: transparent;
}

::-webkit-scrollbar-thumb {
    background-color: var(--border);
    border-radius: var(--radius-full);
    transition: background-color var(--transition-fast);
}

::-webkit-scrollbar-thumb:hover {
    background-color: var(--text-muted);
}

/* Firefox scrollbar */
* {
    scrollbar-width: thin;
    scrollbar-color: var(--border) transparent;
}

/* === FORMS & INPUTS === */

/* ── Form Validation ── */
.valid.modified:not([type=checkbox]) {
    outline: 1px solid var(--success);
    outline-offset: -1px;
}

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

/* ── Blazor Error UI — Escalybook-branded floating toast ── */
#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    max-width: 440px;
    width: calc(100% - 32px);
    background: var(--surface-overlay);
    border: 1px solid var(--error-light);
    border-left: 4px solid var(--error);
    border-radius: 14px;
    padding: var(--space-4) 44px var(--space-4) 56px;
    box-shadow: var(--shadow-lg);
    z-index: 10000;
    font-size: 0.9375rem;
    color: var(--text);
    align-items: center;
    gap: var(--space-3);
}

#blazor-error-ui::before {
    content: '!';
    position: absolute;
    left: 14px;
    top: 50%;
    transform: translateY(-50%);
    width: 28px;
    height: 28px;
    background: var(--error-light);
    color: var(--error);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 800;
    font-size: var(--text-md);
}

#blazor-error-ui .label {
    display: block;
    font-weight: 500;
    line-height: 1.4;
}

#blazor-error-ui .reload {
    display: inline-block;
    margin-top: 6px;
    color: var(--primary);
    font-weight: 600;
    text-decoration: none;
    font-size: var(--text-base);
}

#blazor-error-ui .reload:hover {
    text-decoration: underline;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 12px;
    top: 12px;
    color: var(--text-muted);
    font-size: var(--text-3xl);
    line-height: 1;
    width: 24px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    transition: color 0.2s, background-color 0.2s;
}

#blazor-error-ui .dismiss:hover {
    color: var(--text);
    background: var(--bg-subtle);
}

/* === ANIMATIONS === */

/* ============================================================
   Animations
   ============================================================ */

/* Shimmer / Skeleton Loading */
@keyframes shimmer {
    0% {
        background-position: -200% 0;
    }
    100% {
        background-position: 200% 0;
    }
}

.skeleton {
    background: linear-gradient(
        90deg,
        var(--bg-subtle) 25%,
        var(--border-light) 37%,
        var(--bg-subtle) 63%
    );
    background-size: 200% 100%;
    animation: shimmer 1.5s ease-in-out infinite;
    border-radius: var(--radius-sm);
}

.skeleton-card {
    height: 120px;
    border-radius: var(--radius-lg);
}

/* Page Fade-In */
@keyframes fadeIn {
    from {
        opacity: 0;
        transform: translateY(8px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.fade-in {
    animation: fadeIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}

/* Subtle float for decorative elements */
@keyframes float {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-6px); }
}

/* Pulse for notifications / badges */
@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.5; }
}

/* Spin for loading indicators */
@keyframes spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}

/* Scale on hover */
@keyframes scaleIn {
    from {
        opacity: 0;
        transform: scale(0.95);
    }
    to {
        opacity: 1;
        transform: scale(1);
    }
}

/* Slide up for toasts / alerts */
@keyframes slideUp {
    from {
        opacity: 0;
        transform: translateY(16px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* === LAYOUT === */

/* ============================================================
   Section Spacing & Layout
   ============================================================ */
.section {
    padding: 80px 0;
}

.section-title {
    text-align: center;
    margin-bottom: var(--space-12);
}

.section-title h2,
.section-title h3,
.section-title h4 {
    margin-bottom: var(--space-2);
}

.section-title p {
    max-width: 500px;
    margin: 0 auto;
    color: var(--text-secondary);
}

/* ============================================================
   Stat Card
   ============================================================ */
.stat-card {
    text-align: center;
    padding: var(--space-6);
    border-radius: var(--radius-lg);
    background: var(--surface);
    border: 1px solid var(--border-light);
    box-shadow: var(--shadow-xs);
    transition: box-shadow var(--transition-base), transform var(--transition-base);
}

.stat-card:hover {
    box-shadow: var(--shadow-sm);
}

.stat-card .stat-icon {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-bottom: var(--space-3);
    background: var(--primary-light);
    color: var(--primary-on-tint, var(--primary));
}

.stat-card .stat-icon.success {
    background: var(--success-light);
    color: var(--success-text);
}

.stat-card .stat-icon.warning {
    background: var(--warning-light);
    color: var(--warning-text);
}

.stat-card .stat-icon.error {
    background: var(--error-light);
    color: var(--error);
}

.stat-card .stat-icon.info {
    background: var(--info-light);
    color: var(--info);
}

.stat-value {
    font-size: 2rem;
    font-weight: 700;
    color: var(--text);
    letter-spacing: -0.025em;
    line-height: 1.2;
}

.stat-label {
    color: var(--text-muted);
    font-size: var(--text-sm);
    font-weight: 500;
    margin-top: var(--space-1);
    letter-spacing: 0.01em;
}

/* ============================================================
   Service Card
   ============================================================ */
.service-card {
    cursor: pointer;
    border: 2px solid transparent;
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: var(--shadow-xs);
    transition: border-color var(--transition-base), background-color var(--transition-base),
                box-shadow var(--transition-base), transform var(--transition-base);
    overflow: hidden;
}

.service-card:hover {
    border-color: var(--primary-lighten);
    transform: translateY(-2px);
    box-shadow: var(--shadow-md);
}

.service-card.selected {
    border-color: var(--primary);
    background: var(--primary-light);
    box-shadow: var(--shadow-md), 0 0 0 1px var(--primary);
}

.service-card.selected:hover {
    box-shadow: var(--shadow-lg), 0 0 0 1px var(--primary);
}

/* ============================================================
   Employee Card
   ============================================================ */

/* ============================================================
   Time Slot Pill
   ============================================================ */
.time-slot {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-2) var(--space-5);
    border-radius: var(--radius-full);
    border: 1.5px solid var(--border);
    background: var(--surface);
    color: var(--text);
    font-size: var(--text-base);
    font-weight: 500;
    cursor: pointer;
    transition: border-color var(--transition-base), background-color var(--transition-base),
                color var(--transition-base), box-shadow var(--transition-base),
                transform var(--transition-base);
    user-select: none;
    min-width: 100px;
}

.time-slot:hover {
    border-color: var(--primary-lighten);
    background: var(--primary-light);
    color: var(--primary-on-tint, var(--primary));
    transform: translateY(-1px);
    box-shadow: var(--shadow-sm);
}

.time-slot.selected {
    border-color: var(--primary);
    background: var(--primary);
    color: var(--primary-btn-text);
    box-shadow: 0 4px 12px color-mix(in srgb, var(--primary) 30%, transparent);
}

.time-slot.selected:hover {
    background: var(--primary-hover);
    border-color: var(--primary-hover);
}

.time-slot:disabled,
.time-slot.disabled {
    opacity: 0.4;
    cursor: not-allowed;
    transform: none;
    box-shadow: none;
}

/* ============================================================
   Message Bubble (Support Chat)
   ============================================================ */
.message-bubble {
    max-width: 75%;
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-lg);
    margin-bottom: var(--space-2);
    font-size: 0.9375rem;
    line-height: 1.5;
    animation: slideUp 0.2s cubic-bezier(0.4, 0, 0.2, 1) forwards;
    position: relative;
    word-wrap: break-word;
}

.message-bubble.own {
    background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%);
    color: var(--primary-btn-text);
    margin-left: auto;
    border-bottom-right-radius: 4px;
    box-shadow: 0 2px 8px color-mix(in srgb, var(--primary) 20%, transparent);
}

.message-bubble.other {
    background: var(--bg-subtle);
    color: var(--text);
    border-bottom-left-radius: 4px;
    box-shadow: var(--shadow-xs);
}

.message-bubble .message-time {
    font-size: 0.6875rem;
    opacity: 0.7;
    margin-top: var(--space-1);
    display: block;
}

.message-bubble.own .message-time {
    text-align: right;
    /* Inherits var(--primary-btn-text) from .message-bubble.own so it tracks the
       bubble fill in both modes; .message-bubble .message-time already sets
       opacity: 0.7. A literal white here stayed light-on-light in dark mode. */
}

.message-bubble.other .message-time {
    color: var(--text-muted);
}

/* ============================================================
   Booking Success
   ============================================================ */
.booking-success {
    text-align: center;
    padding: var(--space-12) var(--space-6);
    position: relative;
    overflow: hidden;
}

/* Confetti / celebration pattern background */
.booking-success::before {
    content: '';
    position: absolute;
    inset: 0;
    background-image:
        radial-gradient(circle at 20% 30%, color-mix(in srgb, var(--primary) 8%, transparent) 0%, transparent 50%),
        radial-gradient(circle at 80% 70%, rgba(236, 72, 153, 0.08) 0%, transparent 50%),
        radial-gradient(circle at 50% 10%, color-mix(in srgb, var(--success) 6%, transparent) 0%, transparent 40%),
        radial-gradient(circle at 10% 80%, color-mix(in srgb, var(--warning) 6%, transparent) 0%, transparent 40%);
    pointer-events: none;
    z-index: 0;
}

.booking-success > * {
    position: relative;
    z-index: 1;
}

.booking-success .success-icon {
    width: 80px;
    height: 80px;
    border-radius: 50%;
    background: var(--success-light);
    color: var(--success-text);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-bottom: var(--space-6);
    font-size: 2.5rem;
    animation: scaleIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}

.booking-success h2 {
    margin-bottom: var(--space-2);
}

.booking-success p {
    color: var(--text-secondary);
    max-width: 400px;
    margin: 0 auto;
}

/* ============================================================
   Feature Card (Landing)
   ============================================================ */
.feature-card {
    text-align: center;
    padding: var(--space-8);
    border-radius: var(--radius-lg);
    background: var(--surface);
    border: 1px solid var(--border-light);
    transition: border-color var(--transition-base), box-shadow var(--transition-base),
                transform var(--transition-base);
}

.feature-card:hover {
    transform: translateY(-4px);
    box-shadow: var(--shadow-lg);
    border-color: var(--border);
}

.feature-card .feature-icon {
    width: 64px;
    height: 64px;
    border-radius: var(--radius-md);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-bottom: var(--space-4);
    font-size: var(--text-4xl);
}

/* ============================================================
   Page Content & Container Utilities
   ============================================================ */
.page-header {
    margin-bottom: var(--space-8);
}

.page-header h1,
.page-header h2 {
    margin-bottom: var(--space-1);
}

.page-header p {
    color: var(--text-secondary);
    margin: 0;
}

/* ── The staff page header (Mobbin wave 3, A8 and I3, 2026-10-19) ──
   ONE header for every dashboard page (Components/Shared/PageHeader.razor): a title, an optional
   muted line, the page's actions, and on a sub-page a back arrow. EXACTLY TWO title sizes: a page
   (--text-2xl) and a sub-page reached through the back arrow (--text-lg). The left edge is the
   layout's: a page never adds sideways padding of its own (PageHeaderGuardTests), which is how half
   the dashboard used to start 16px further in than the other half on a phone. */
.bk-page-header {
    margin: 0 0 var(--space-4);
}

/* The title line: the arrow and the title on the left, the actions on the right. */
.bk-page-header__top {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-3);
}

/* The arrow and the title: one unit sized by its content, so a short title keeps the actions on
   its line and a long one takes the row (wrapping inside it) while the actions wrap under it. */
.bk-page-header__main {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    flex: 1 1 auto;
    min-width: 0;
}

/* Under a back arrow the muted line starts where the title starts. */
.bk-page-header--detail .bk-page-header__sub {
    padding-left: var(--space-10);
}

/* The arrow's glyph lines up with the page's left edge (the 48px button carries 12px of padding)
   and its centre with the title's first line; it never pushes the title down. */
.bk-page-header__back {
    flex: 0 0 auto;
    margin: calc(-1 * var(--space-3)) 0 calc(-1 * var(--space-3)) calc(-1 * var(--space-3));
}

.bk-page-header__title-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
    flex: 1 1 auto;
    min-width: 0;
}

.bk-page-header .bk-page-title {
    margin: 0;
    font-size: var(--text-2xl);
    font-weight: 700;
    line-height: 1.25;
    letter-spacing: -0.01em;
    color: var(--text);
    overflow-wrap: anywhere;
}

.bk-page-header--detail .bk-page-title {
    font-size: var(--text-lg);
}

.bk-page-header .bk-page-subtitle {
    margin: var(--space-1) 0 0;
    font-size: var(--text-base);
    line-height: 1.45;
    color: var(--text-muted);
}

.bk-page-header__actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-left: auto;
}

/* The phone (operator decision 2026-10-20, from production): the title, then the muted line,
   then the actions in a row of their own: the primary (filled) button takes the width, the
   secondary ones sit beside it as icons (a labelled one shows its icon only; the label stays
   its accessible name). A secondary with no icon wraps under the primary instead of squeezing it. Before this the buttons broke under the title and the
   muted line fell below them (Troškovi). The title line stops being a box (display: contents) so
   its two parts can be ordered around the muted line. A header whose only actions are icons (a
   CSV export, a ⋯ menu) has no primary to give a row to and keeps them on the title line. */
@media (max-width: 599px) {
    .bk-page-header:has(> .bk-page-header__top > .bk-page-header__actions > .mud-button-filled:not(.d-none)) {
        display: flex;
        flex-direction: column;
    }

    .bk-page-header:has(> .bk-page-header__top > .bk-page-header__actions > .mud-button-filled:not(.d-none)) > .bk-page-header__top {
        display: contents;
    }

    .bk-page-header__actions:has(> .mud-button-filled:not(.d-none)) {
        order: 3;
        width: 100%;
        margin: var(--space-3) 0 0;
    }

    .bk-page-header__actions > .mud-button-filled:not(.d-none) {
        order: -1;
        flex: 1 1 auto;
        min-width: 0;
    }

    .bk-page-header__actions:has(> .mud-button-filled:not(.d-none)) > .mud-button:not(.mud-button-filled):has(.mud-button-icon-start) .mud-button-label {
        font-size: 0;
    }

    .bk-page-header__actions:has(> .mud-button-filled:not(.d-none)) > .mud-button:not(.mud-button-filled) .mud-button-icon-start {
        margin: 0;
    }
}

/* ============================================================
   Badge / Status Chip
   ============================================================ */

/* ============================================================
   Empty State
   ============================================================ */
.empty-state {
    text-align: center;
    padding: var(--space-16) var(--space-6);
    color: var(--text-muted);
}

.empty-state .empty-icon {
    font-size: 3rem;
    margin-bottom: var(--space-4);
    opacity: 0.5;
}

.empty-state h3 {
    color: var(--text);
    margin-bottom: var(--space-2);
}

.empty-state p {
    max-width: 400px;
    margin: 0 auto;
}

/* === NAVIGATION === */

/* ── Switch/Toggle: minimal overrides ── */
.mud-switch .mud-button-root:hover {
    background-color: transparent !important;
}

/* ── PREKIDAC TEME: jedno dugme, isto na svakoj strani ─────────────────────────────────────
   ⚠️ KEEP IN SYNC with StaticLanding/pricing.html and StaticLanding/landing.html.
   ThemeSwitchParityTests pins the three copies; do not edit one of them alone.

   WHY THERE ARE THREE COPIES AT ALL. landing.html and pricing.html are self-contained
   documents served by middleware before Blazor is consulted — they cannot link this file, and
   a static page that has to fetch a stylesheet to draw its own header is the thing they exist
   to avoid. So the DECLARATIONS are duplicated on purpose and a guard keeps them equal, the
   same call the price strings already carry.

   WHAT THIS FIXES. Components/Shared/ThemeToggle.razor was a MudIconButton with a Material
   glyph: no border, no pill, a different icon pair and a different size from the button on the
   landing page it sits one click away from. Tapping the wordmark on /login therefore changed
   the shape of the theme control, which reads as two different products (the same failure
   ThemeSwitchParityTests was written for, one boundary further out).

   ⚠️ WHICH ICON SHOWS IS DECIDED BY CSS, NOT BY C#, and that is deliberate: data-theme on
   <html> is written by theme-boot.js, the single writer, so the Razor button and the static
   pages resolve the same state from the same attribute. A C# `IsDarkMode ? moon : sun` would
   be a second reader that can disagree with the document during the first render.

   ⚠️ landing.html's copy keeps ITS OWN colour tokens (--rail-line) and that is not drift: its
   switch sits on the dark purple rail, where --border is invisible. Geometry, the icon rules
   and the [hidden] rule are what the guard pins. */
.theme-switch[hidden]{display:none}
.theme-switch{
  display:inline-flex;align-items:center;justify-content:center;
  width:44px;height:44px;flex-shrink:0;cursor:pointer;
  background:none;border:1px solid var(--border);border-radius:var(--radius-full);color:var(--text-muted);
  -webkit-appearance:none;appearance:none;
  transition:background .15s ease,color .15s ease;
}
@media (hover:hover){.theme-switch:hover{background:var(--surface-hover);color:var(--primary)}}
.theme-switch:active{background:var(--surface-hover)}
.theme-switch svg{width:20px;height:20px}
/* Sunce se vidi u tamnoj (ponuda: upali svetlo), mesec u svetloj. */
.theme-switch .i-sun{display:none}
.theme-switch .i-moon{display:block}
:root[data-theme="dark"] .theme-switch .i-sun{display:block}
:root[data-theme="dark"] .theme-switch .i-moon{display:none}

/* ============================================================
   MudBlazor Overrides
   ============================================================ */

/* ── AppBar: glass effect ── */
.mud-appbar {
    backdrop-filter: blur(12px) saturate(180%);
    -webkit-backdrop-filter: blur(12px) saturate(180%);
    background: var(--surface-glass-strong) !important;
    border-bottom: 1px solid var(--border-light);
    box-shadow: none !important;
    transition: background var(--transition-base);
}

.mud-appbar.mud-appbar-dense {
    min-height: 56px;
}

/* The PUBLIC app bar (PublicLayout: the Blazor /pricing, /book and /t pages) is solid, with no
   blur, the same as pricing.html's header (operator decision 2026-10-22). The signed-in bars
   keep the glass above. */
.mud-appbar.escalybook-appbar-autohide {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: var(--surface) !important;
}

/* ── Drawer: clean sidebar ── */
.mud-drawer {
    border-right: 1px solid var(--border-light) !important;
    box-shadow: none !important;
    background: var(--surface) !important;
    height: 100%;
    overflow-y: auto;
}

.mud-drawer .mud-nav-link {
    border-radius: var(--radius-sm);
    margin: 2px 0;
    padding: 10px var(--space-4);
    font-size: var(--text-base);
    font-weight: 500;
    color: var(--text-secondary);
    transition: background-color var(--motion-fast) var(--ease-standard),
                color var(--motion-fast) var(--ease-standard);
}

.mud-drawer .mud-nav-link:hover {
    background: var(--bg-subtle);
    color: var(--text);
}

.mud-drawer .mud-nav-link.active {
    background: var(--primary-light);
    color: var(--primary-on-tint, var(--primary));
    font-weight: 600;
}

.mud-drawer .mud-nav-link.active .mud-nav-link-icon {
    color: var(--primary);
}

.mud-drawer .mud-navmenu.mud-navmenu-bordered .mud-nav-link:not(:last-child)::after {
    display: none;
}

/* ── Nav Group ── */
.mud-nav-group .mud-nav-group-title {
    font-size: var(--text-base);
    font-weight: 500;
}

/* ── Table: modern styling ── */
.mud-table {
    border-radius: var(--radius-md) !important;
    border: 1px solid var(--border-light) !important;
    box-shadow: var(--shadow-xs) !important;
    overflow: hidden;
}

.mud-table .mud-table-head .mud-table-row .mud-table-cell {
    background: var(--bg-subtle) !important;
    border-bottom: 1px solid var(--border) !important;
    font-weight: 600;
    font-size: var(--text-sm);
    color: var(--text-secondary);
    padding: var(--space-3) var(--space-4);
}

.mud-table .mud-table-body .mud-table-row .mud-table-cell {
    border-bottom: 1px solid var(--border-light) !important;
    padding: var(--space-3) var(--space-4);
    font-size: var(--text-base);
    vertical-align: middle;
}

.mud-table .mud-table-body .mud-table-row:last-child .mud-table-cell {
    border-bottom: none !important;
}

.mud-table .mud-table-body .mud-table-row:hover {
    background: var(--bg-subtle) !important;
}

/* ── Chip: pill shape ── */
.mud-chip {
    border-radius: var(--radius-full) !important;
    font-weight: 500;
    font-size: var(--text-sm);
    transition: background-color var(--motion-fast) var(--ease-standard),
                color var(--motion-fast) var(--ease-standard),
                box-shadow var(--motion-fast) var(--ease-standard);
}

.mud-chip:hover {
    box-shadow: var(--shadow-sm);
}

/* ── Card: consistent radius and shadow ── */
.mud-card {
    border-radius: var(--radius-lg) !important;
    box-shadow: var(--shadow-sm) !important;
    border: 1px solid var(--border-light);
    transition: background-color var(--motion-fast) var(--ease-standard),
                box-shadow var(--motion-fast) var(--ease-standard);
    overflow: hidden;
}

.mud-card:hover {
    box-shadow: var(--shadow-md) !important;
}

.mud-card.mud-elevation-0 {
    box-shadow: none !important;
}

.mud-card.mud-elevation-0:hover {
    box-shadow: none !important;
}

/* ── Button: smoother transitions ──
   2026-11-03 apple-design s1: the eased transform transition lives on the BASE rule, so the release
   (the way back from scale(.97)) is the eased 160ms one; the press itself is the fast 100ms one on
   :active below. The `!important` on `transition` stays because MudBlazor sets its own
   `transition` on .mud-button-root and loads after this file's earlier rules.
   ⚠️ Named properties, never `all` (audit 2026-10-15 MOT-21): `all` animated padding and size
   when a label changed and box-shadow on every press, on the most-used control in the product. */
.mud-button-root {
    transition: background-color var(--motion-fast) var(--ease-standard),
                color var(--motion-fast) var(--ease-standard),
                border-color var(--motion-fast) var(--ease-standard),
                box-shadow var(--motion-fast) var(--ease-standard),
                transform 160ms cubic-bezier(.32, .72, 0, 1) !important;
    text-transform: none !important;
    font-weight: 500;
    letter-spacing: 0;
    border-radius: var(--radius-sm) !important;
}

.mud-button-root:hover {
    transform: translateY(-1px);
}

.mud-button-root:active {
    /* 2026-11-03 apple-design s1: the only :active rule for buttons (the duplicate far below was
       merged here). Fast press, eased release. No `!important` on transform: nothing else sets a
       transform on .mud-button-root:active; `transition` needs it to beat the base list above. */
    transform: scale(0.97);
    /* Full list, only the transform timing differs from the base rule, so colour, border and
       shadow keep easing while pressed. */
    transition: background-color var(--motion-fast) var(--ease-standard),
                color var(--motion-fast) var(--ease-standard),
                border-color var(--motion-fast) var(--ease-standard),
                box-shadow var(--motion-fast) var(--ease-standard),
                transform 100ms ease-out !important;
}

.mud-button-root.mud-button-filled {
    box-shadow: var(--shadow-sm);
}

.mud-button-root.mud-button-filled:hover {
    box-shadow: var(--shadow-md);
}

.mud-button-root.mud-button-outlined {
    border-width: 1.5px;
}

/* Prevent icon-only buttons from moving */
.mud-icon-button:hover {
    transform: none;
}

/* ── Dialog ──
   ⚠️ NO backdrop-filter / filter / transform / will-change / contain on .mud-dialog or anything
   inside it. Any of them makes the dialog the containing block for its `position: fixed`
   descendants — and a MudDatePicker with PickerVariant.Dialog renders its calendar as a FIXED
   MudOverlay INSIDE the dialog's content, not in a portal. Reported from production 2026-09-25
   ("ne može da doda slobodan dan"): the phone bottom sheet is ~230px tall, so the calendar's
   overlay shrank to the sheet, the calendar was centred in it and clipped, and no day past the
   third week could be reached. Measured at 390x760: overlay 232px with the blur, 760px without.
   The blur this rule used to carry did nothing visible — the dialog paints an opaque surface, so
   there is nothing behind it to blur. Pinned by DialogContainingBlockGuardTests. */
.mud-dialog {
    border-radius: var(--radius-lg) !important;
    box-shadow: var(--shadow-xl) !important;
    border: 1px solid var(--border-light);
}

.mud-dialog .mud-dialog-title {
    font-weight: 600;
    font-size: var(--text-lg);
    padding: var(--space-4) var(--space-5) var(--space-2);
}

.mud-dialog .mud-dialog-content {
    padding: var(--space-2) var(--space-5);
}

.mud-dialog .mud-dialog-actions {
    padding: var(--space-2) var(--space-5) var(--space-4);
}

/* Dialog backdrops ONLY (.mud-overlay-dialog). MudBlazor v9 selects/menus open a
   modal popover with a bare .mud-overlay click-catcher stacked ABOVE the dialog —
   styling all .mud-overlay blurs/paints over the dialog the dropdown lives in. */
.mud-overlay.mud-overlay-dialog {
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
}

/* ── The bottom sheet is a PHONE shape only (PWA step 2, decision 4) ──
   ShowResponsiveAsync opens every non-full-screen dialog at DialogPosition.BottomCenter, because
   that container class is the only CSS hook a shared helper HAS: MudBlazor 9.1's DialogOptions
   carries no Class, and .mud-dialog's class comes from each dialog's own <MudDialog> tag — 89 of
   them. On a phone that position IS the sheet. On anything wider it would drop every dialog in the
   product to the bottom edge of the screen, so MudBlazor's own bottomcenter rule is put back to
   centre here. Same specificity as MudBlazor's (0,2,0); this file loads after MudBlazor.min.css,
   which is what makes it win without !important. */
@media (min-width: 600px) {
    .mud-dialog-container.mud-dialog-bottomcenter {
        align-items: center;
        padding-bottom: 0;
    }
}

/* ── Text Field: cleaner inputs ── */
.mud-input.mud-input-outlined .mud-input-outlined-border {
    border-radius: var(--radius-sm);
    border-width: 1.5px;
    transition: border-color var(--transition-fast);
}

.mud-input-control:hover .mud-input.mud-input-outlined .mud-input-outlined-border {
    border-color: var(--primary-lighten);
}

/* Placeholder ink (audit 2026-10-15 APP-04). MudBlazor 9.1 paints it currentColor at .42
   opacity: 2.62:1 light, 3.48:1 dark, and some fields carry their only format hint there.
   --text-muted measures 6.56 light / 5.22 dark. Same selectors and specificity as MudBlazor's
   own rule; app.css loads after it, so this wins without !important. */
.mud-input > input.mud-input-root::placeholder,
div.mud-input-slot.mud-input-root::placeholder,
.mud-input > textarea.mud-input-root::placeholder {
    color: var(--text-muted);
    opacity: 1;
}

/* A placeholder or value longer than its field ends in an ellipsis instead of being cut mid-word
   (Klijenti and Zaposleni search on a phone, 2026-11-02). Every single-line MudBlazor input. */
.mud-input > input.mud-input-root,
.mud-input > input.mud-input-root::placeholder {
    text-overflow: ellipsis;
}

/* ── Paper: consistent styling ── */
.mud-paper {
    border-radius: var(--radius-md);
}

/* ── Paper: the elevation policy, applied where a call site cannot be reached ───────────────
   MudPaper and MudCard default to Elevation="1". Pass Elevation="0" at the call site wherever
   you can — this rule is for the ones inside MudBlazor's own composites.

   ⚠️ Do NOT try to zero MudBlazor's Shadows array globally instead. That array also drives
   menus, popovers and dialogs, which are exactly the things that SHOULD float; flattening it
   would remove the one signal that separates a popover from the page under it.

   Scoped to .mud-elevation-1 on purpose: a call site that deliberately asked for 2 or more
   wanted to float and keeps its shadow. */
/* ⚠️ !important, because MudBlazor 9.1.0's OWN stylesheet ships
   `.mud-elevation-1 { box-shadow: ... !important }`. Without it this rule adds the border and
   loses the shadow fight, leaving the paper with BOTH — the exact double-separation the policy
   exists to remove. The guard could not see it either: WinningShadow reads app.css and the vendor
   sheet is not in it. (Codex gate, blazor#429.) */
.mud-paper.mud-elevation-1 {
    box-shadow: var(--elev-flat) !important;
    border: 1px solid var(--border);
}

/* ⚠️ A CARD needs its own rule, and it needs !important, because the card rules above already
   assign their shadows that way. `.mud-paper.mud-elevation-1` matches a MudCard — a card IS a
   paper — but it loses the cascade to `.mud-card { box-shadow: ... !important }` and the card
   went on floating at rest and lifting on hover. Found by the Codex gate on blazor#429, which
   also noted the guard could not see it: a `Contains` check proves the declaration is present,
   never that it WINS.

   !important is matched here rather than removed from the rules above, because those two lines
   are load-bearing for every card that did not ask for elevation 0 and are not this change's to
   rewrite. Scoped to elevation-1 exactly like the paper rule: a call site that asked for 2 or
   more wanted to float and keeps its shadow. */
.mud-card.mud-elevation-1 {
    box-shadow: var(--elev-flat) !important;
    border: 1px solid var(--border);
}

.mud-card.mud-elevation-1:hover {
    box-shadow: var(--elev-flat) !important;
}

/* ── Alert: softer styling ── */
.mud-alert {
    border-radius: var(--radius-sm) !important;
}

/* ── Alert ink: bind the text variants to OUR tokens (D25) ────────────────────────────────
   MudBlazor's text-variant alert colours itself from --mud-palette-{severity}, which the
   theme contract (R1) forbids us from aliasing — those are MudBlazor's own scale and
   MudThemeProvider does not exist at first paint, nor at all on the static pages. So each
   severity is bound explicitly to the app.css token instead, which is the one the rest of
   the design system already uses.

   !important is required, not stylistic: MudBlazor emits its own
   `.mud-alert-text-{severity} { color: ... }` from the component stylesheet, and the two
   selectors have identical specificity — without it the load order decides the colour, and
   the load order is not ours to control from a class library.

   The icon is a SEPARATE rule in MudBlazor 9.x (`.mud-alert-text-{severity} .mud-alert-icon`),
   so it needs its own binding or the icon and the words disagree. */
.mud-alert-text-warning,
.mud-alert-text-warning .mud-alert-icon { color: var(--warning-text) !important; }

.mud-alert-text-error,
.mud-alert-text-error .mud-alert-icon { color: var(--error-text) !important; }

.mud-alert-text-success,
.mud-alert-text-success .mud-alert-icon { color: var(--success-text) !important; }

.mud-alert-text-info,
.mud-alert-text-info .mud-alert-icon { color: var(--info-text) !important; }

/* ── Avatar: smooth transitions ── */
.mud-avatar {
    transition: transform var(--motion-fast) var(--ease-standard);
}

/* ── Snackbar: modern styling ── */
.mud-snackbar {
    border-radius: var(--radius-sm) !important;
    box-shadow: var(--shadow-lg) !important;
}

/* Below 960px the fixed bottom nav and the FAB (bottom: 80px) sit where MudBlazor puts a
   BottomRight toast (right: 24px; bottom: 24px), so a tap on the bar during a toast hit the
   toast (audit 2026-10-15 APP-16). Full width with a gutter, lifted above both. */
@media (max-width: 959px) {
    .mud-snackbar-location-bottom-right {
        left: 12px !important;
        right: 12px !important;
        bottom: calc(144px + env(safe-area-inset-bottom)) !important;
    }
}

/* ── Date Picker styling ── */
.mud-picker .mud-picker-calendar-header {
    font-weight: 600;
}

/* ── Tooltip: cleaner look ── */
.mud-tooltip {
    border-radius: var(--radius-sm) !important;
    font-size: var(--text-sm);
    padding: 6px var(--space-3);
}

/* ── Progress / Loading ── */
.mud-progress-linear {
    border-radius: var(--radius-full) !important;
}

/* ── Tab: cleaner tabs ── */
/* Pretplata: two equal tabs across the whole width at every phone width, with their words and no
   scrolling. MudTabs holds each tab at a 160px minimum beside scroll buttons, and wraps each one in
   a fit-content element, so the columns are set on those wrappers. */
.billing-tabs .mud-tabs-tabbar-content .mud-tabs-tabbar-wrapper { width: 100%; transform: none !important; }
.billing-tabs .mud-tabs-tabbar-wrapper > :not(.mud-tab-slider) { flex: 1 1 0; min-width: 0; width: auto !important; display: block; }
.billing-tabs .mud-tabs-scroll-button { display: none; }
.billing-tabs .mud-tab { min-width: 0 !important; width: 100%; padding-inline: var(--space-2); gap: var(--space-2); }
.billing-tab-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 22px;
    height: 22px;
    padding: 0 6px;
    border-radius: var(--radius-full);
    background: var(--error-light);
    color: var(--error-text);
    font-size: var(--text-xs);
    font-weight: 700;
    line-height: 1;
}

/* The operator's outage sentence, at the top of the dashboard. It wraps; a long word breaks. */
.staff-notice {
    padding: var(--space-3) 16px;
    background: var(--warning-light);
    color: var(--warning-text);
    border-bottom: 1px solid var(--warning);
    font-size: var(--text-sm);
    font-weight: 600;
    overflow-wrap: anywhere;
}

/* Pretplata: the open faktura at the top of the page. */
.billing-due {
    border: 1px solid var(--border-light);
    border-radius: var(--radius-lg);
    padding: var(--space-5);
    margin-bottom: var(--space-6);
}
.billing-due--late { border-color: var(--error); }

/* Pretplata: the faktura that comes next. */
.billing-next-lines { margin: var(--space-3) 0 0; font-size: var(--text-sm); color: var(--text-muted); }
.billing-next-line { display: flex; justify-content: space-between; gap: var(--space-3); padding: var(--space-1) 0; }
.billing-next-line dt { min-width: 0; overflow-wrap: anywhere; }
.billing-next-line dd { margin: 0; white-space: nowrap; color: var(--text); font-variant-numeric: tabular-nums; }
.billing-amount {
    font-size: var(--text-2xl);
    font-weight: 700;
    color: var(--text);
    margin-top: var(--space-2);
    font-variant-numeric: tabular-nums;
}
.billing-amount--list { font-size: var(--text-xl); }

.mud-tabs .mud-tab {
    text-transform: none !important;
    font-weight: 500;
    letter-spacing: 0;
}

/* === UTILITIES === */

/* ============================================================
   Utility Classes
   ============================================================ */
/* ── The three that carry the design language ──────────────────────────────────────────────
   Small, boring, and between them they are most of what separates a screen that reads as
   expensive from one that reads as a template. Use these rather than re-declaring the same
   three property sets on every page. */

/* Every number, count, duration and clock time. Invisible until it is missing: without it a
   column of money jitters because the digits are proportionally spaced, which is why a default
   MudBlazor table does not line up and Fresha's does. */
.num {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
}

.bg-subtle {
    background: var(--bg-subtle);
}

.rounded-lg {
    border-radius: var(--radius-lg);
}

/* ============================================================
   Status Chips (by booking status)
   Text is the *-text ink, never the hue (APP-14: the hue as text failed AA).
   ============================================================ */
.status-chip--confirmed {
    background: var(--status-confirmed-bg);
    color: var(--status-confirmed-text);
}

.status-chip--completed {
    background: var(--status-completed-bg);
    color: var(--status-completed-text);
}

.status-chip--canceled {
    background: var(--status-canceled-bg);
    color: var(--status-canceled-text);
}

.status-chip--noshow {
    background: var(--status-noshow-bg);
    color: var(--status-noshow-text);
}

.status-chip--rescheduled {
    background: var(--status-rescheduled-bg);
    color: var(--status-rescheduled-text);
}

.status-chip--default {
    background: var(--status-pending-bg);
    color: var(--status-pending);
}

/* ============================================================
   Status chips, every NON-booking status (Mobbin wave 2, I1, 2026-10-19)
   Rendered by src/Escalybook.Core.Ui/Components/Shared/StatusChip.razor; contract and reasons in
   docs/agents/theme-app-css.md "Status chips". StatusChipCssGuardTests pins each tone to exactly
   its token pair and refuses a themed or nested override: the tokens carry the theme.
   ============================================================ */
.bk-status-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    min-height: 22px;
    padding: 2px 10px;
    border-radius: var(--radius-full);
    font-size: var(--text-xs);
    font-weight: 600;
    line-height: 1.4;
    letter-spacing: 0.01em;
    white-space: nowrap;
    vertical-align: middle;
    /* A chip never shrinks below its own text: inside a flex cell next to a long figure it was
       squeezed a few pixels and clipped its label (found on the firma detail invoices, P-H). */
    flex-shrink: 0;
}

.bk-status-chip .mud-icon-root {
    font-size: 14px;
}

.bk-status-chip--neutral {
    background: var(--pill-neutral-bg);
    color: var(--pill-neutral-text);
}

.bk-status-chip--good {
    background: var(--success-light);
    color: var(--success-text);
}

.bk-status-chip--warning {
    background: var(--warning-light);
    color: var(--warning-text);
}

.bk-status-chip--danger {
    background: var(--error-light);
    color: var(--error-text);
}

.bk-status-chip--info {
    background: var(--info-light);
    color: var(--info-text);
}

/* Progress track (Mobbin wave 3, D6): a thin bar for "this much of that", drawn by
   src/Escalybook.Core.Ui/Components/Shared/ProgressTrack.razor. Tokens only, so both themes. */
.pt-track {
    display: block;
    height: 6px;
    border-radius: var(--radius-full);
    background: var(--border-light);
    overflow: hidden;
}

.pt-fill {
    display: block;
    height: 100%;
    border-radius: inherit;
    background: var(--primary);
}

/* Filter chips (Mobbin wave 2, B2/B4): the native pill toggles above a list */
/* The ONE small section label of the dashboard (Kalendar's „TERMINI”, DayAgenda .ag-title).
   Every page uses this class rather than its own copy, so the design cannot drift per page. */
.bk-eyebrow {
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--text-muted);
}

/* Two form fields side by side from 600px, stacked on a phone. */
.dlg-two {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-3);
}

@media (min-width: 600px) {
    .dlg-two {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    }
}

.bk-filter-chips {
    display: flex;
    gap: var(--space-2);
    flex-wrap: wrap;
}

.bk-filter-chip {
    min-height: 44px;
    padding: 6px 14px;
    border-radius: var(--radius-full);
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text-muted);
    font-family: inherit;
    font-size: var(--text-sm);
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
}

.bk-filter-chip--active {
    background: var(--primary-hover);
    color: var(--primary-btn-text);
    border-color: var(--primary-hover);
}


/* Status select pill (for inline status dropdowns) */

/* ============================================================
   Table Premium (unified table styling)
   ============================================================ */
.table-premium .mud-table-head .mud-table-row .mud-table-cell {
    border-bottom: 1px solid var(--border-light) !important;
    background: var(--table-header-bg) !important;
    font-size: 0.68rem;
    font-weight: 600;
    color: var(--text-muted);
    padding: var(--space-3) var(--space-4);
}

.table-premium .mud-table-body .mud-table-row .mud-table-cell {
    border-bottom: 1px solid var(--bg) !important;
    font-size: var(--text-base);
    color: var(--text-secondary);
    padding: var(--space-3) var(--space-4);
}

.table-premium .mud-table-body .mud-table-row:hover {
    background-color: var(--bg) !important;
}

.table-premium .mud-table-body .mud-table-row:last-child .mud-table-cell {
    border-bottom: none !important;
}

/* ============================================================
   Card Responsive (stacks content on mobile)
   ============================================================ */
.card-responsive {
    background: var(--surface);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-lg);
    padding: var(--space-4);
    transition: box-shadow var(--transition-base);
}

.card-responsive:hover {
    box-shadow: var(--shadow-sm);
}

/* ── The view a ResponsiveView has ALREADY chosen ──
   Layout only, and deliberately NO media query. `.mobile-card-list` / `.desktop-table` below
   switch on a CSS breakpoint; ResponsiveView switches on MudBlazor's viewport service, which reads
   `window.innerWidth`. Those two arbiters do not agree: `innerWidth` INCLUDES the scrollbar and a
   media query does not, so on a desktop with a classic scrollbar there is a band around 600px
   where MudBlazor says "wide" and the media query says "narrow". With both in play the component
   renders the desktop branch and the stylesheet hides it — a blank list, at one specific window
   width, which is about as hard to reproduce a bug as this codebase can produce.
   So content inside a ResponsiveView wears these instead, and nothing here touches `display`
   conditionally. Do not add a media query to them. */
.bk-view-stack {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.bk-view-block {
    display: block;
}

/* ⚠️ STILL LIVE, for the four /admin/* pages that have not moved to ResponsiveView
   (BusinessCategories, DunningPage, InvoicesPage, TenantsList). They are WebAssembly routes, so
   they pay CPU for the hidden branch but no SignalR payload. When the last of them converts, this
   pair and its media query go with it. */
/* Mobile card list (replaces table on small screens) */
.mobile-card-list {
    display: none;
}

.desktop-table {
    display: block;
}

@media (max-width: 599px) {
    .mobile-card-list {
        display: flex;
        flex-direction: column;
        gap: var(--space-3);
    }

    .desktop-table {
        display: none;
    }

    .bk-filter-chips--scroll {
        flex-wrap: nowrap;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
    }

    /* A sideways chip strip ends in a short fade instead of a hard cut, so a chip half out of
       view reads as "more to the side", the way a native chip strip does; the end padding lets
       the last chip scroll fully clear of the fade. */
    .bk-filter-chips--scroll,
    .scroll-fade-end {
        padding-right: var(--space-6);
        -webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--space-6)), transparent);
        mask-image: linear-gradient(to right, #000 calc(100% - var(--space-6)), transparent);
    }

    .bk-filter-chips--scroll::-webkit-scrollbar {
        display: none;
    }

    .bk-filter-chips--scroll .bk-filter-chip {
        flex: 0 0 auto;
    }

    /* A sideways strip always rests on a chip's start, never with a chip half cut at the left
       edge: picking a chip makes it bold and wider, and the browser then nudged the strip 11px
       so „Sve” sat cut beside „Danas”. */
    .bk-filter-chips--scroll,
    .scroll-fade-end {
        scroll-snap-type: x mandatory;
        scroll-padding-inline: 0 var(--space-6);
    }

    .bk-filter-chips--scroll > *,
    .scroll-fade-end > * {
        scroll-snap-align: start;
    }

    /* Where the browser can tie an animation to the scroll position, the left edge fades too
       once the strip has moved: right fade at the start, both in the middle, left at the end
       (the last chips can never snap to the left edge, so the one before them would sit cut). */
    @supports (animation-timeline: scroll()) {
        .bk-filter-chips--scroll,
        .scroll-fade-end {
            animation: bk-strip-fade linear both;
            animation-timeline: scroll(self inline);
        }
    }
}

@keyframes bk-strip-fade {
    0% {
        -webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--space-6)), transparent);
        mask-image: linear-gradient(to right, #000 calc(100% - var(--space-6)), transparent);
    }
    2%, 98% {
        -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--space-6), #000 calc(100% - var(--space-6)), transparent);
        mask-image: linear-gradient(to right, transparent, #000 var(--space-6), #000 calc(100% - var(--space-6)), transparent);
    }
    100% {
        -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--space-6));
        mask-image: linear-gradient(to right, transparent, #000 var(--space-6));
    }
}

/* ============================================================
   Touch Target
   ============================================================ */
.touch-target {
    min-width: var(--touch-target-min);
    min-height: var(--touch-target-min);
}

/* Alias. Five components in Escalybook.Core.Ui write `tap-target` rather than
   `touch-target` — SegmentedControl, UpcomingBookingCard, StickyDateHeader,
   Home and Login. That class was never defined anywhere, so all five silently
   got no minimum size at all. Defining the alias repairs them without touching
   the RCL; the call sites can be renamed later at leisure. */
.tap-target {
    min-width: var(--touch-target-min);
    min-height: var(--touch-target-min);
}

/* ============================================================
   Press feedback
   ------------------------------------------------------------
   body sets -webkit-tap-highlight-color: transparent, so a tap produces no
   native flash anywhere in the app. The public booking wizard got a press
   primitive when it was fixed, but it was declared inside that page's own
   <style> block — and Escalybook.Core.Ui ships no stylesheet of its own, so the
   class was physically unreachable from every other page. Hoisting it here
   makes it available app-wide: shells, calendar, list cards, dialogs.
   Transform + background only, so it composites and can never trigger layout.
   ============================================================ */
.bw-pressable {
    transition: transform 90ms cubic-bezier(.2, .7, .3, 1),
                background-color 180ms cubic-bezier(.2, .7, .3, 1),
                border-color 180ms cubic-bezier(.2, .7, .3, 1);
}

.bw-pressable:active {
    transform: scale(0.97);
}

/* Wide targets (full-width rows, nav items, bottom-bar buttons) need a
   shallower scale — 0.97 on a 360px-wide element reads as the page flinching. */
.bw-pressable--wide:active {
    transform: scale(0.985);
}

@media (prefers-reduced-motion: reduce) {
    .bw-pressable:active,
    .bw-pressable--wide:active {
        transform: none;
        background-color: var(--primary-light);
    }
}

/* ============================================================
   Coarse-pointer touch target floor
   ------------------------------------------------------------
   Measured live on production 2026-08-01 at /admin/invoices:
     6x icon button   25x25px   nearest-neighbour gap  4px
     4x filter chip  ~51-98x32px             gap 12px
     11 of 14 small controls sit closer than 16px to another button.
   Same VISUAL-AUDIT-2026-07-31.md sweep: Size.Small buttons render 26-28px
   tall platform-wide (21 on /dashboard/settings alone, 9 on /admin/marketing).

   .tap-target (above) already floors five specific call sites at 44px, but
   applying that unconditionally to all 516 Size.Small instances across 126
   core.ui files would inflate every dense desktop admin table — nobody asked
   for a density change, and MudBlazor's own small-size classes are what most
   controls actually carry, not .tap-target.

   Why this can't be a hit-area pseudo-element instead: every MudBlazor button
   carries .mud-ripple { position:relative; overflow:hidden } and the ripple
   effect itself is implemented with generated content on the same box — a
   hit-extending generated-content layer would collide with it, and
   overflow:hidden clips anything outside the border box for hit-testing
   anyway. So growth has to happen in the LAYOUT BOX, not a phantom overlay.
   Consequence: hit area == border box, always. No generated-content hit
   extensions here, no negative margins — flex/table layout tiles border
   boxes disjointly, so two grown controls push each other apart instead of
   overlapping. At the measured 25x25px/4px case, a naive generated-content
   hit box centred on each icon would put two 44px targets 29px apart
   centre-to-centre — a 15px-wide overlap. Real layout growth cannot produce
   that: it can only widen the gap.

   Scope is the PRIMARY-pointer coarse media feature, never the broader
   "matches if ANY attached pointer is coarse" variant. A touchscreen laptop
   matches that broader variant but keeps a fine primary pointer (mouse is
   primary) and deliberately keeps desktop density under this rule — its
   occasional screen taps still meet the un-floored 25px targets, which is
   accepted because a precise pointer is present. Phones and detached tablets
   have coarse as their primary pointer and get the floor. iPads showing
   desktop tables grow under this — that IS the point, do not shrink the floor
   to avoid it. (Note the growth is not all new: the older
   `@media (hover: none) and (pointer: coarse)` block far below already floors
   every `.mud-icon-button` at 44x44 and every `.mud-button-root` at 44px tall,
   both with !important, on exactly those devices. What this block adds is
   min-WIDTH on small text / outlined / filled buttons, clickable chips, and
   coverage of coarse-primary devices that still report hover.)

   Chip scope is deliberately `.mud-chip.mud-clickable` only, never bare
   `.mud-chip` — non-interactive status chips (e.g. an inline height:22px badge
   in an invoice row) would be inflated by this too, because min-height beats
   an inline height declaration.

   The `.mud-input`/`.mud-input-adornment` exclusion sits OUTSIDE this media
   query and carries !important, and both of those are load-bearing. MudBlazor
   9.1.0 declares `.mud-input { display:inline-flex; align-items:center }` with
   no height, and `.mud-input-adornment { height:.01em; max-height:2em }` — so
   a 44px floor on a clear "x" or an adornment button either stretches the
   whole field or overflows the height-locked wrapper, on every dense search
   box (e.g. /admin/invoices). Three separate floors would otherwise beat a
   scoped, non-important carve-out: `.mud-button-root` (min-height) and
   `.mud-icon-button` (both axes), each 44px !important under
   `(hover: none) and (pointer: coarse)`, plus
   `.mud-icon-button.mud-icon-button-size-small` at 44px under
   `(max-width: 599px)` — that last one is equal in specificity but later in
   the file, so it wins on order. Between them they cover every phone and
   tablet, which is to say the carve-out had NO effect anywhere before it was
   given !important. It is scoped tightly to descendants of an input; nothing
   outside a field can match it.

   .action-cluster exists so a handful of call sites can widen 4px gaps to 8px
   under this same media query without a second, competing gap rule fighting
   app.css's own `gap-1` utility at equal specificity (link order would
   silently decide the winner). Base gap matches gap-1 (--space-1) so desktop
   is unchanged; only the coarse-pointer branch below widens it.
   ============================================================ */
.action-cluster {
    display: flex;
    align-items: center;
    gap: var(--space-1);
}

@media (pointer: coarse) {
    .mud-icon-button-size-small,
    .mud-button-text-size-small,
    .mud-button-outlined-size-small,
    .mud-button-filled-size-small,
    .mud-chip.mud-clickable {
        min-width: var(--touch-target-min);
        min-height: var(--touch-target-min);
    }

    .action-cluster {
        gap: var(--space-2);
    }
}

/* Buttons living INSIDE an input (clear "x", adornments) must stay unfloored.
   Unconditional and !important on purpose — see comment block above. */
.mud-input .mud-icon-button,
.mud-input-adornment .mud-icon-button {
    min-width: unset !important;
    min-height: unset !important;
}

/* ============================================================
   Dialog Modern (consistent dialog styling)
   ============================================================ */
.dialog-modern .mud-dialog-content {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
    padding: var(--space-4) var(--space-6) var(--space-6) !important;
}

.dialog-modern .mud-dialog-title {
    font-weight: 700;
    font-size: var(--text-lg);
    padding: var(--space-5) var(--space-6) var(--space-2);
    letter-spacing: -0.01em;
}

.dialog-modern .mud-dialog-actions {
    padding: var(--space-3) var(--space-6) var(--space-5);
    display: flex;
    justify-content: flex-end;
    gap: var(--space-3);
}

.dialog-modern .mud-input-label {
    font-weight: 600;
    font-size: var(--text-sm);
    color: var(--text-secondary);
}

@media (max-width: 599px) {
    .dialog-modern .mud-dialog-content {
        gap: var(--space-4);
        padding: var(--space-3) var(--space-4) var(--space-4) !important;
    }
}

/* ============================================================
   Empty State with SVG Illustration
   ============================================================ */

/* === DASHBOARD === */

/* ============================================================
   Početna (dashboard home), variant A, 2026-10-30
   ============================================================ */
/* One head line, then the grid. Below 900px the grid is a phone column: the rail is
   display:contents there so its cards take their own place around the day agenda (order below);
   from 900px it is the right column. The week grid left this page for /dashboard/kalendar on
   2026-10-31 (Kalendar tok, decision 2). Tokens only. */
/* A size container: the layout below asks how wide the CONTENT is, never the viewport. With the
   260px sidebar open a 1280px window leaves ~970px for the page (Codex on #892), where three
   columns clipped the week's day headers. The FAB and the dialogs live in the layout, outside
   this box, so the containment changes nothing for them. */
.home { display: flex; flex-direction: column; gap: var(--space-3); container-type: inline-size; }
/* The canvas's head: breathing room above the day, a rule under the line. */
.home-head { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3) 0 var(--space-3); border-bottom: 1px solid var(--border-light); }
.home-head-row { display: flex; align-items: flex-start; gap: var(--space-2); }
.home-head-text { flex: 1; min-width: 0; }
.home-title { margin: 0; font-size: var(--text-xl); font-weight: 700; letter-spacing: -0.01em; color: var(--text); line-height: 1.2; }
.home-sub { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: 2px; font-size: var(--text-sm); color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.home-dot { color: var(--text-muted); }
.home-today { min-height: 44px; padding: 0 var(--space-3); border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--surface); color: var(--primary); font-size: var(--text-sm); font-weight: 600; cursor: pointer; font-family: inherit; }
.home-today:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.home-new { flex-shrink: 0; }
/* The quiet money line: naplaćeno bold, očekivano muted (it is a forecast, not cash), the api#690
   sentences under it in the same voice. */
.home-money { display: flex; flex-direction: column; gap: 2px; font-size: var(--text-base); color: var(--text); }
.home-money-main { display: flex; flex-wrap: wrap; gap: var(--space-1); }
/* „Naplaćeno” opens the line: ::first-letter needs a block container, so the span is inline-block. */
.home-money-main > span:first-child { display: inline-block; }
.home-money-main > span:first-child::first-letter { text-transform: capitalize; }
.home-money-main strong { color: var(--text); font-weight: 700; }
.home-money-muted { color: var(--text-muted); }
.home-money-notes { display: flex; flex-wrap: wrap; gap: var(--space-3); font-size: var(--text-xs); color: var(--text-muted); }
.home-money-notes strong { color: var(--text-secondary); font-weight: 700; }

.home-grid { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }
.home-grid > *, .home-rail > * { min-width: 0; }

/* Two layouts, by content width (container queries on .home):
     under 900px        the phone column: the rail is display:contents so its cards take their own
                        place (order) around the agenda;
     from 900px         two columns, agenda | rail (a laptop with the sidebar open, or a tablet). */
@container (max-width: 899px) {
    .home-rail { display: contents; }
    .home-agenda { order: 2; }
    .home-rail > .um-card { order: 1; }
    .home-rentals { order: 3; }
    .home-kpi { order: 4; }
    .home-failed { order: 5; }
}

@container (min-width: 900px) {
    .home-grid { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: var(--space-4); align-items: start; }
    .home-agenda { grid-column: 1; grid-row: 1; }
    .home-rail { display: flex; flex-direction: column; gap: var(--space-3); grid-column: 2; grid-row: 1; }
    .home-failed { grid-column: 1 / -1; grid-row: 2; }
}

/* The sheets behind „Treba uraditi” (Ideja nedelje, the physio lists). Moved here from
   WeeklyIdeaDialog on 2026-10-31 so PhysioTodoDialog reads the same list, not a copy of it. */
/* No side padding of its own: MudDialog's content already carries it, and the two stacked
   pushed a 390px sheet's rows into wrapping. */
.wd { padding: 0 0 var(--space-2); display: flex; flex-direction: column; gap: var(--space-2); }
.wd-sub { margin: 0; font-size: var(--text-sm); color: var(--text-secondary); }
.wd-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.wd-item { border-top: 1px solid var(--border-light); }
.wd-row { display: flex; align-items: center; gap: var(--space-2); width: 100%; min-height: 52px; padding: var(--space-2) 0; border: 0; background: transparent; color: var(--text); text-align: left; cursor: pointer; font: inherit; }
.wd-row:disabled { opacity: 0.6; cursor: progress; }
.wd-row:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: var(--radius-sm); }
.wd-avatar { width: 36px; height: 36px; border-radius: 18px; flex-shrink: 0; background: var(--primary-light); color: var(--primary); font-size: var(--text-sm); font-weight: 700; display: flex; align-items: center; justify-content: center; }
/* Name and meta each keep to one line (the meta carries the figures, the spend at its end)
   and clip, so a row stays 52px on a phone instead of growing on a wrapped „dana”. */
.wd-who { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.wd-name { font-size: var(--text-sm); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wd-meta { font-size: var(--text-xs); color: var(--text-secondary); font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wd-chevron { color: var(--text-muted); flex-shrink: 0; }
.wd-row--static { cursor: default; }
.wd-days { font-size: var(--text-xs); font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
.wd-more { margin: 0; font-size: var(--text-xs); color: var(--text-muted); }
.wd-actions { padding: var(--space-2) 0 var(--space-2); display: flex; justify-content: flex-end; gap: var(--space-2); width: 100%; }
/* The picked patient (stomatologija): facts in two columns, the message as a bubble, the buttons
   wrapping on a phone. */
.wd-back { display: inline-flex; align-items: center; gap: var(--space-1); align-self: flex-start; min-height: 44px; padding: 0 var(--space-2) 0 0; border: 0; background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-sm); cursor: pointer; }
.wd-back:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: var(--radius-sm); }
.wd-pick-name { margin: 0; font-size: var(--text-base); font-weight: 700; color: var(--text); }
.wd-facts { margin: 0; display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2) var(--space-3); }
.wd-facts div { display: flex; flex-direction: column; gap: 2px; }
.wd-facts dt { font-size: var(--text-xs); color: var(--text-muted); }
.wd-facts dd { margin: 0; font-size: var(--text-sm); color: var(--text); font-variant-numeric: tabular-nums; }
.wd-msg-title { margin: var(--space-2) 0 0; font-size: var(--text-xs); font-weight: 600; color: var(--text-muted); }
.wd-bubble { padding: var(--space-3); border-radius: var(--radius-md); background: var(--bg-warm); color: var(--text); font-size: var(--text-sm); line-height: 1.45; overflow-wrap: anywhere; }
.wd-note { margin: 0; font-size: var(--text-xs); color: var(--text-muted); }
.wd-hold { margin: 0; font-size: var(--text-xs); color: var(--warning-text); }
.wd-pick-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; padding-top: var(--space-2); }
.wd-call { display: inline-flex; align-items: center; gap: var(--space-1); min-height: 44px; padding: 0 var(--space-3); border: 1px solid var(--primary); border-radius: var(--radius-md); color: var(--primary); font-weight: 600; font-size: var(--text-sm); text-decoration: none; white-space: nowrap; }
.wd-tag { font-size: var(--text-xs); font-weight: 600; padding: 2px 7px; border-radius: var(--radius-sm); white-space: nowrap; flex-shrink: 0; }
.wd-tag--warning { background: var(--warning-light); color: var(--warning-text); }
.wd-tag--muted { background: var(--bg-subtle); color: var(--text-secondary); }

/* „Treba uraditi”: the owner's two physio rows. */
.ptr-item { padding: 0; }
.ptr-button { display: flex; align-items: center; gap: var(--space-2); width: 100%; min-height: 52px; padding: var(--space-2) 0; border: 0; background: transparent; color: var(--text); text-align: left; cursor: pointer; font: inherit; font-size: var(--text-sm); }
.ptr-button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: var(--radius-sm); }
.ptr-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ptr-sub { font-size: var(--text-xs); color: var(--text-secondary); }

/* Dan terapeuta, Dan vlasnika (Components/Physio/PhysioDay.razor). */
.pd { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }
.pd-head { display: flex; align-items: center; gap: var(--space-1); }
.pd-head-text { flex: 1; min-width: 0; display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); padding: 0 2px; }
.pd-title { font-size: var(--text-xs); font-weight: 600; color: var(--text-muted); }
.pd-hint { font-size: var(--text-xs); color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pd-nav { color: var(--primary); }
.pd-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.pd-chip { min-height: 44px; padding: 0 var(--space-3); border-radius: var(--radius-sm); border: 1.5px solid var(--border); background: var(--surface); color: var(--text-secondary); font: inherit; font-size: var(--text-sm); font-weight: 600; cursor: pointer; }
.pd-chip[aria-pressed="true"] { background: var(--primary); border-color: var(--primary); color: var(--primary-btn-text); }
.pd-chip:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.pd .mud-button-root { min-height: 44px; }
.pd-empty { padding: var(--space-5) 0; font-size: var(--text-base); color: var(--text-secondary); text-align: center; }
.pd-card, .pd-list, .pd-table { border: 1px solid var(--border-light); border-radius: var(--radius-md); background: var(--surface); }
.pd-list { list-style: none; margin: 0; padding: 0 var(--space-3); display: flex; flex-direction: column; }
.pd-row + .pd-row { border-top: 1px solid var(--border-light); }
.pd-open { display: flex; align-items: center; gap: var(--space-2); width: 100%; min-height: 64px; padding: 6px 0; border: 0; background: transparent; color: var(--text); text-align: left; cursor: pointer; font: inherit; }
.pd-open:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; border-radius: var(--radius-sm); }
.pd-time { width: 44px; flex-shrink: 0; font-size: var(--text-sm); font-weight: 600; font-variant-numeric: tabular-nums; color: var(--text); }
.pd-time--past { color: var(--text-muted); }
.pd-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.pd-line { display: flex; align-items: center; gap: 6px; min-width: 0; }
.pd-name { font-size: var(--text-sm); font-weight: 600; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pd-tag { font-size: var(--text-xs); font-weight: 600; padding: 2px 7px; border-radius: var(--radius-sm); white-space: nowrap; flex-shrink: 0; }
.pd-tag--warning { background: var(--warning-light); color: var(--warning-text); }
.pd-tag--primary { background: var(--primary-light); color: var(--primary); }
.pd-tag--success { background: var(--success-light); color: var(--success-text); }
.pd-tag--muted { background: var(--bg-subtle); color: var(--text-secondary); }
.pd-tag--error { background: var(--error-light); color: var(--error-text); }
.pd-sub { font-size: var(--text-xs); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pd-track { height: 4px; }
.pd-count { flex-shrink: 0; min-width: 52px; text-align: right; white-space: nowrap; font-size: var(--text-sm); font-weight: 700; font-variant-numeric: tabular-nums; color: var(--primary); }
.pd-count:empty { min-width: 0; }
/* The last visit under a tapped row. */
.pd-past { margin: 0 0 var(--space-3) 52px; padding: 10px var(--space-3); border-radius: var(--radius-md); background: var(--bg-warm); display: flex; flex-direction: column; gap: 6px; }
.pd-past-title { font-size: var(--text-xs); font-weight: 600; color: var(--text-muted); }
.pd-past-line { margin: 0; font-size: var(--text-sm); color: var(--text); overflow-wrap: anywhere; }
.pd-past-line--muted { color: var(--text-secondary); }
.pd-past-actions { display: flex; gap: var(--space-2); margin-top: 2px; }
.pd-btn { min-height: 44px; padding: 0 var(--space-3); border-radius: var(--radius-md); font: inherit; font-size: var(--text-sm); font-weight: 600; cursor: pointer; }
.pd-past-actions .pd-btn--filled { flex: 1; }
.pd-btn--filled { border: 0; background: var(--primary); color: var(--primary-btn-text); }
.pd-btn--outline { flex-shrink: 0; border: 1.5px solid var(--border); background: var(--surface); color: var(--primary); }
.pd-btn:focus-visible, .pd-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.pd-link { align-self: flex-start; min-height: 44px; padding: 0; border: 0; background: transparent; color: var(--primary); font: inherit; font-size: var(--text-sm); font-weight: 600; cursor: pointer; }
/* „Bez beleške”. */
.pd-card { padding: 0 var(--space-3); }
.pd-missing { display: flex; align-items: center; gap: var(--space-2); width: 100%; min-height: 52px; padding: var(--space-2) 0; border: 0; background: transparent; color: var(--text); text-align: left; cursor: pointer; font: inherit; font-size: var(--text-sm); }
.pd-missing:focus-visible, .pd-missing-row:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; border-radius: var(--radius-sm); }
.pd-missing-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.pd-chevron { color: var(--text-muted); flex-shrink: 0; }
.pd-chevron--open { transform: rotate(180deg); }

/* Fizioterapija: Karton A (Components/Physio/PhysioKartonPanel.razor). Container query, because the
   width that matters is the karton's, not the viewport's: a dental firma's right column takes 280px
   of it. Below 900px of its own width the columns dissolve and the card, tabs and parts order
   themselves in one flex column; every part stays mounted so a read is never repeated on a tab. */
.pk { container-type: inline-size; min-width: 0; }
.pk-grid { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }
.pk-col { display: contents; }
.pk-card { order: 0; }
.pk-tabs { order: 1; display: flex; gap: var(--space-2); overflow-x: auto; scrollbar-width: none; }
.pk-tabs::-webkit-scrollbar { display: none; }
.pk-tab { flex: 0 0 auto; white-space: nowrap; }
.pk-part { order: 2; display: none; flex-direction: column; gap: var(--space-3); min-width: 0; }
.pk-part--on { display: flex; }
@container (min-width: 900px) {
    .pk-grid { display: grid; grid-template-columns: 280px minmax(0, 1fr) 340px; gap: var(--space-4); align-items: start; }
    .pk-col { display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; }
    .pk-tabs { display: none; }
    .pk-part { display: flex; }
}
@media print {
    .pk-col { display: block; }
    .pk-part { display: flex; }
}
.pk-card { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3) var(--space-4); border: 1px solid var(--border-light); border-radius: var(--radius-md); background: var(--surface); min-width: 0; }
.pk-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); flex-wrap: wrap; }
.pk-count { font-size: var(--text-xl); font-weight: 700; color: var(--primary); font-variant-numeric: tabular-nums; white-space: nowrap; }
.pk-next { font-size: var(--text-sm); color: var(--text-secondary); }
.pk-track { height: 6px; }
.pk-stats { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); }
.pk-stat { display: flex; flex-direction: column; min-width: 64px; }
.pk-stat-n { font-size: var(--text-base); font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
.pk-stat-l { font-size: var(--text-xs); color: var(--text-secondary); }
.pk-actions { display: flex; gap: var(--space-2); }
.pk-actions .pd-btn--filled { flex: 1; }
.pk-muted { margin: 0; font-size: var(--text-sm); color: var(--text-secondary); }

/* „Bol danas, 0 do 10” (PhysioPainGrid): six cells a row, 44px high, on a phone and a desktop alike. */
.ppg { display: flex; flex-direction: column; gap: 6px; }
.ppg-label { font-size: var(--text-sm); font-weight: 600; color: var(--text); }
.ppg-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 6px; }
.ppg-cell { height: 44px; border: 1.5px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--text-secondary); font: inherit; font-size: var(--text-sm); font-weight: 600; font-variant-numeric: tabular-nums; cursor: pointer; }
.ppg-cell[aria-checked="true"] { background: var(--primary); border-color: var(--primary); color: var(--primary-btn-text); }
.ppg-cell:disabled { opacity: 0.6; cursor: default; }
.ppg-cell:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.ppg-hint { font-size: var(--text-xs); color: var(--text-secondary); }

/* „Serija”, variant B (PhysioSeriesDialog). */
.ss { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }
.ss-sub { margin: 0; font-size: var(--text-sm); color: var(--text-secondary); overflow-wrap: anywhere; }
.ss-head { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
.ss-track { flex: 1; min-width: 40px; }
.ss-rhythm { flex-shrink: 0; font-size: var(--text-sm); color: var(--text-secondary); white-space: nowrap; }
.ss-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.ss-item + .ss-item { border-top: 1px solid var(--border-light); }
.ss-row { display: flex; align-items: center; gap: var(--space-2); width: 100%; min-height: 56px; padding: 6px 0; border: 0; background: transparent; color: var(--text); text-align: left; font: inherit; cursor: pointer; }
.ss-row:disabled { color: var(--text); cursor: default; }
.ss-row:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; border-radius: var(--radius-sm); }
.ss-n { width: 24px; flex-shrink: 0; text-align: center; font-size: var(--text-sm); font-weight: 700; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.ss-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ss-when { font-size: var(--text-sm); font-weight: 600; }
.ss-line { margin: 0; font-size: var(--text-xs); color: var(--text-secondary); }
.ss-open { margin: 0 0 var(--space-2) 32px; padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); background: var(--bg-warm); display: flex; flex-direction: column; gap: var(--space-2); }
.ss-note { display: grid; grid-template-columns: 1.4rem minmax(0, 1fr); gap: 2px var(--space-2); margin: 0; font-size: var(--text-sm); color: var(--text); }
.ss-note dt { font-weight: 700; color: var(--primary); }
.ss-note dd { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.ss-btn { min-height: 44px; text-transform: none; font-weight: 600; }

/* „Nova serija” (PhysioNewSeriesDialog) and „Završi seriju” (PhysioFinishSeriesDialog). */
.ns, .fs { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }
.ns-banner { padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); background: var(--primary-light); color: var(--primary); font-size: var(--text-sm); font-weight: 600; overflow-wrap: anywhere; }
.ns-card { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3); border: 1px solid var(--border-light); border-radius: var(--radius-md); background: var(--surface); min-width: 0; }
.ns-when { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); }
/* Date and time one per line on a phone: side by side the date read „03.11.20…”. */
@media (max-width: 599px) { .ns-when { grid-template-columns: minmax(0, 1fr); } }
.ns-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); min-height: 44px; color: var(--text); }
.ns-label { font-size: var(--text-sm); font-weight: 600; color: var(--text); }
.ns-stepper { display: inline-flex; align-items: center; gap: var(--space-2); }
.ns-count { min-width: 32px; text-align: center; font-size: var(--text-lg); font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
.ns-days { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 2px; }
.ns-day { padding: 0; }
.ns-preview { font-size: var(--text-sm); color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.ns-toggle { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); width: 100%; min-height: 44px; padding: 0; border: 0; background: transparent; color: var(--text); text-align: left; font: inherit; cursor: pointer; }
.ns-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: var(--radius-sm); }
.ns-toggle-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ns-toggle-title { font-size: var(--text-sm); font-weight: 600; }
.ns-toggle-sub { font-size: var(--text-xs); color: var(--text-secondary); }
.ns-knob { position: relative; flex-shrink: 0; width: 44px; height: 26px; border-radius: 13px; background: var(--border); transition: background var(--transition-fast); }
.ns-knob::after { content: ""; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 10px; background: var(--surface); transition: left var(--transition-fast); }
.ns-toggle[aria-checked="true"] .ns-knob { background: var(--primary); }
.ns-toggle[aria-checked="true"] .ns-knob::after { left: 21px; }
.ns-packs { display: flex; flex-direction: column; gap: 6px; }
.ns-pack-choice { text-align: left; padding: var(--space-2) var(--space-3); overflow-wrap: anywhere; }
.ns-foot { margin: 0; font-size: var(--text-xs); color: var(--text-secondary); }
.ns-skipped { margin: 0; padding-left: var(--space-4); font-size: var(--text-sm); color: var(--text); }
.fs-status { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.pd-missing-list { list-style: none; margin: 0; padding: 0; }
.pd-missing-row { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; width: 100%; min-height: 52px; padding: var(--space-2) 0; border: 0; border-top: 1px solid var(--border-light); background: transparent; color: var(--text); text-align: left; cursor: pointer; font: inherit; }
/* „Serije sada”. */
.pd-now { padding: var(--space-3); display: flex; flex-direction: column; gap: 6px; }
.pd-now-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); }
.pd-now-grid > div { display: flex; flex-direction: column; }
.pd-now-num { font-size: var(--text-lg); font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text); }
.pd-now-label { font-size: var(--text-xs); color: var(--text-muted); }
/* The owner's table, from 900px of content; the list under it. */
.pd-table { display: none; overflow: hidden; }
.pd-tr { display: grid; grid-template-columns: 52px minmax(0, 1.1fr) minmax(0, 1.2fr) minmax(0, 0.7fr) 92px 104px; gap: var(--space-3); align-items: center; width: 100%; min-height: 60px; padding: 6px var(--space-3); border: 0; border-bottom: 1px solid var(--border-light); background: var(--surface); color: var(--text); text-align: left; cursor: pointer; font: inherit; font-size: var(--text-sm); }
.pd-tr:last-child { border-bottom: 0; }
.pd-tr--head { min-height: 36px; font-size: var(--text-xs); font-weight: 600; color: var(--text-muted); cursor: default; }
.pd-tr--on { background: var(--primary-tint); }
.pd-tr:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.pd-cell { color: var(--text-secondary); min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pd-series { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.pd-series .pd-count { text-align: left; min-width: 0; }
/* „Izabrani pacijent”. */
.pd-panel { padding: var(--space-3); display: flex; flex-direction: column; gap: var(--space-2); }
.pd-panel-name { font-size: var(--text-md); font-weight: 700; color: var(--text); }
.pd-panel .pd-sub { white-space: normal; }
.pd-facts { margin: 0; display: flex; flex-direction: column; font-size: var(--text-sm); }
.pd-facts > div { display: flex; justify-content: space-between; gap: var(--space-3); padding: 7px 0; border-top: 1px solid var(--border-light); }
.pd-facts dt { color: var(--text-secondary); }
.pd-facts dd { margin: 0; font-weight: 600; text-align: right; color: var(--text); }
.pd-panel-note { margin: 0; font-size: var(--text-xs); color: var(--text-muted); }

@container (max-width: 899px) {
    .home-selected { display: none; }
}

@container (min-width: 900px) {
    .pd-list--manager { display: none; }
    .pd-table { display: block; }
}

/* A free calendar cell (Kalendar tok, decision 3, 2026-10-31; Components/Dashboard/FreeCell.razor):
   invisible until hovered or focused, under every block and chip (those carry z-index 1). No text
   selection and no iOS callout, so a long press on it is only the long press. */
.free-cell { position: absolute; left: 0; right: 0; margin: 0; padding: 0; border: 0; background: transparent; cursor: pointer; appearance: none; -webkit-appearance: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
.free-cell:not(:disabled):hover { background: var(--primary-tint); }
.free-cell:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; z-index: 2; }
.free-cell:disabled { cursor: default; }

/* ============================================================
   Notification Bell
   ============================================================ */
.notification-badge {
    position: relative;
}

.notification-badge-count {
    position: absolute;
    top: 4px;
    right: 4px;
    min-width: 18px;
    height: 18px;
    border-radius: 9px;
    background: var(--error);
    color: var(--text-inverse);
    font-size: 0.65rem;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 var(--space-1);
    border: 2px solid var(--surface);
    animation: chipPop 0.25s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}

.notification-dropdown {
    width: 360px;
    max-width: 100vw;
    max-height: 420px;
    overflow-y: auto;
    padding: 0;
}

.notification-item {
    display: flex;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--border-light);
    transition: background var(--transition-fast);
    cursor: pointer;
}

.notification-item:hover {
    background: var(--bg-subtle);
}

.notification-item:last-child {
    border-bottom: none;
}

.notification-item.unread {
    background: var(--primary-tint);
}

/* ============================================================
   Command Palette (Ctrl+K)
   ============================================================ */
.command-palette-overlay {
    position: fixed;
    inset: 0;
    z-index: 2000;
    background: var(--overlay-scrim);
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding-top: 20vh;
    /* No entrance (audit 2026-10-15 MOT-22): a keyboard-invoked palette appears instantly, and
       fadeIn's translateY slid the full-screen scrim itself. */
}

.command-palette {
    width: 100%;
    max-width: 560px;
    background: var(--surface);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-xl);
    border: 1px solid var(--border);
    overflow: hidden;
}

.command-palette:focus-within {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
}

.command-palette-input {
    width: 100%;
    padding: var(--space-4) var(--space-5);
    border: none;
    outline: none;
    font-size: var(--text-md);
    font-family: inherit;
    background: transparent;
    color: var(--text);
    border-bottom: 1px solid var(--border-light);
}

.command-palette-input::placeholder {
    color: var(--text-muted);
}

.command-palette-results {
    max-height: 320px;
    overflow-y: auto;
    padding: var(--space-2);
}

.command-palette-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: 10px var(--space-3);
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: background var(--transition-fast);
    font-size: var(--text-base);
    color: var(--text);
}

.command-palette-item:hover,
.command-palette-item.active {
    background: var(--primary-light);
    color: var(--primary-on-tint, var(--primary));
}

@media (max-width: 599px) {
    .command-palette-overlay {
        padding-top: 10vh;
        padding: var(--space-4);
    }

    .command-palette {
        max-width: 100%;
    }
}

/* === CUSTOMER PORTAL === */

/* ============================================================
   Customer Side Panel
   ============================================================ */
.customer-panel-header {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    padding: var(--space-8) var(--space-6) var(--space-6);
    border-bottom: 1px solid var(--border-light);
}

.customer-panel-stats {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-3);
    padding: var(--space-5) var(--space-6);
    border-bottom: 1px solid var(--border-light);
}

.customer-panel-stat {
    text-align: center;
}

.customer-panel-stat-value {
    font-size: var(--text-xl);
    font-weight: 700;
    color: var(--text);
}

.customer-panel-stat-label {
    font-size: var(--text-xs);
    color: var(--text-muted);
    margin-top: 2px;
}

/* ============================================================
   Employee Calendar (Fresha-style day view)
   ============================================================ */

/* ============================================================
   Chat-Style Support Tickets
   ============================================================ */
.chat-thread {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-4);
    max-height: calc(100vh - 300px);
    max-height: calc(100dvh - 300px);
    overflow-y: auto;
}

@media (max-width: 599px) {
    .chat-thread {
        max-height: calc(100dvh - 280px);
    }
}

.chat-reply-box {
    position: sticky;
    bottom: 0;
    background: var(--surface);
    border-top: 1px solid var(--border-light);
    padding: var(--space-4);
    display: flex;
    gap: var(--space-3);
    align-items: flex-end;
}

.chat-reply-box .mud-input-control {
    flex: 1;
}

/* ⚠️ LOAD-BEARING — do not delete with the next dead-CSS sweep.
   A `transform`, `will-change` or `contain` on an ancestor makes it the containing block for its
   `position: fixed` descendants, which then scroll with it instead of with the viewport. MudBlazor
   sets those on the layout elements, so this resets them.
   This comment used to name the FAB, and the FAB is gone — but the rule is not orphaned with it.
   What depends on it now is `.mobile-bottom-nav` (DashboardLayout), which is `position: fixed` and
   is on screen for every phone user of /dashboard/*, plus the fixed public app bar. Delete this and
   the bottom bar rides up the page on scroll. */
.mud-layout,
.mud-main-content,
.mud-layout-content {
    transform: none !important;
    will-change: auto !important;
    contain: none !important;
}

/* Session expired dialog — never full-screen, always centered card */
.session-expired-dialog {
    max-height: none !important;
    height: auto !important;
    max-width: 400px !important;
    width: auto !important;
    border-radius: var(--radius-lg) !important;
    margin: auto !important;
}

/* === RESPONSIVE === */

/* ============================================================
   Responsive Breakpoints
   sm: 640px | md: 768px | lg: 1024px | xl: 1440px
   ============================================================ */

/* ── Phone (< 600px) ── */
@media (max-width: 599px) {
    html, body {
        font-size: 14px;
    }

    /* 16px inputs on phones: iOS zooms the page on focus into any field under 16px, and the
       14px root above made every MudBlazor input 14px. This used to be hidden behind
       user-scalable=no, which also blocked pinch-zoom (audit 2026-10-15 WIZ-04/APP-24). Same
       selectors and specificity as MudBlazor 9.1's own `font: inherit` rule, later in the
       cascade. The booking wizard forces 16px on its own inputs already. */
    .mud-input > input.mud-input-root,
    .mud-input > textarea.mud-input-root,
    div.mud-input-slot.mud-input-root,
    /* The two native text fields outside MudBlazor (message reply box, habits board), which
       the Mud selectors above cannot reach and which were 13-14px here (review of package B). */
    .mt-input,
    .hb-input {
        font-size: 16px;
    }

    .section {
        padding: var(--space-12) 0;
    }

    .section-title {
        margin-bottom: var(--space-8);
    }

    .message-bubble {
        max-width: 85%;
    }

    .booking-success {
        padding: var(--space-8) var(--space-4);
    }

    /* Stack customer layout nav on mobile */
    .mud-appbar .mud-button-root {
        padding: 6px var(--space-2);
        font-size: var(--text-xs);
    }
}

/* ── Small (640px - 767px) ── */
@media (min-width: 640px) and (max-width: 767px) {

    .section {
        padding: 56px 0;
    }
}

/* ── Medium (768px - 1023px) ── */
@media (min-width: 768px) and (max-width: 1023px) {

    .section {
        padding: var(--space-16) 0;
    }
}

/* ── Large (1024px - 1439px) ── */
@media (min-width: 1024px) and (max-width: 1439px) {

    .section {
        padding: 72px 0;
    }
}

/* ── Extra Large (1440px+) ── */
@media (min-width: 1440px) {

    .section {
        padding: 80px 0;
    }
}

/* ── Hover effects only for non-touch devices ── */
@media (hover: hover) and (pointer: fine) {

    .service-card:hover {
        transform: translateY(-2px);
        box-shadow: var(--shadow-md);
    }

    .time-slot:hover {
        transform: translateY(-1px);
    }
}

/* ── Touch devices: remove hover transforms ──
   On touch, :hover latches after a tap and only clears when the next tap lands
   elsewhere, so a lifted card stays lifted with no way to un-hover.
   .an-stat-card is the dashboard's only hover-transform (the authenticated
   pages express hover as colour, not movement) — verified by grep rather than
   assumed, so this list stays honest instead of speculatively long. */
@media (hover: none) {
    .tenant-card:hover,
    .service-card:hover,
    .employee-card:hover,
    .time-slot:hover,
    .an-stat-card:hover,
    .mud-button-root:hover {
        transform: none;
    }
}

/* ── Reduced motion ── */
@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }

    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* ── Print ── */
@media print {
    .mud-appbar,
    .mud-drawer,
    .mud-snackbar-provider,
    #blazor-error-ui {
        display: none !important;
    }

    .mud-main-content {
        padding: 0 !important;
    }

    .glass-card,
    .mud-card {
        box-shadow: none !important;
        border: 1px solid #ccc !important;
    }
}

/* ============================================================
   LOGIN PAGE — Split Screen Premium Design
   ============================================================ */

.login-page-centered {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: calc(100vh - 56px);
    min-height: calc(100dvh - 56px);
    background: linear-gradient(135deg, var(--bg) 0%, var(--primary-tint) 100%);
    padding: var(--space-6);
}

.login-form-wrapper {
    width: 100%;
    max-width: 440px;
}

.login-form-card {
    background: var(--surface);
    border-radius: var(--radius-md);
    padding: var(--space-10) 36px;
    box-shadow: var(--shadow-sm);
    border: 1px solid var(--border-light);
}

.login-field-group {
    margin-bottom: var(--space-5);
}

/* Make MudTextField inputs taller in login form */
.login-input .mud-input-outlined .mud-input-slot {
    padding-top: 14px;
    padding-bottom: 14px;
}

.login-input .mud-input.mud-input-outlined .mud-input-outlined-border {
    border-radius: 14px !important;
}

.login-input .mud-input-control:hover .mud-input.mud-input-outlined .mud-input-outlined-border {
    border-color: var(--primary-lighten) !important;
}

.login-input .mud-input.mud-input-outlined.mud-input-adorned-start .mud-input-slot {
    padding-left: var(--space-3);
}

.login-error {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: 10px 14px;
    background: var(--error-light);
    border: 1px solid var(--error-light);
    border-radius: var(--radius-md);
    margin-bottom: var(--space-1);
}

/* /registracija: first and last name side by side on the login card; one under the other on a
   narrow phone, where two half-width fields would clip their labels. */
.register-name-row {
    display: flex;
    gap: var(--space-3);
}

.register-name-row > * {
    flex: 1 1 0;
    min-width: 0;
}

@media (max-width: 420px) {
    .register-name-row {
        flex-direction: column;
        gap: var(--space-5);
    }
}

.login-submit-btn:hover:not(:disabled) {
    box-shadow: 0 6px 20px color-mix(in srgb, var(--primary) 50%, transparent) !important;
    transform: translateY(-1px);
}

.login-submit-btn:active:not(:disabled) {
    transform: translateY(0);
}

.login-submit-btn.mud-button-filled.mud-disabled,
.login-submit-btn[disabled] {
    background-color: var(--primary) !important;
    color: var(--primary-btn-text) !important;
    opacity: 0.5;
    pointer-events: none;
}

/* Desktop: wider form panel */
@media (min-width: 960px) {

    .login-form-card {
        padding: var(--space-12) 44px;
    }
}

@media (max-width: 599px) {
    .login-form-card {
        padding: 28px var(--space-5);
        border-radius: var(--radius-lg);
    }
}

/* ============================================================
   Booking Wizard Calendar
   ============================================================ */
.wizard-calendar-wrapper {
    display: flex;
    justify-content: center;
}

.wizard-calendar-wrapper .mud-picker {
    width: 100%;
    max-width: 380px;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    overflow: hidden;
    box-shadow: none;
}

.wizard-calendar-wrapper .mud-picker-calendar-header {
    padding: var(--space-3) var(--space-4) var(--space-1);
}

.wizard-calendar-wrapper .mud-picker-calendar-header-switch {
    font-weight: 600;
    font-size: 0.9375rem;
    color: var(--text);
}

.wizard-calendar-wrapper .mud-picker-calendar .mud-button-root {
    border-radius: var(--radius-sm);
    font-weight: 500;
    transition: background-color 0.15s ease, color 0.15s ease;
}

.wizard-calendar-wrapper .mud-picker-calendar .mud-button-root:hover {
    transform: none;
}

.wizard-calendar-wrapper .mud-picker-calendar .mud-picker-calendar-day.mud-selected {
    font-weight: 700;
}

.wizard-calendar-wrapper .mud-picker .mud-picker-content {
    padding: 0 var(--space-3) var(--space-3);
}

/* ============================================================
   Page Transitions & Micro-animations
   ============================================================ */

/* Page enter animation - apply to main content wrappers.
   NOTE: no `forwards` fill and no transform in the resting state — a retained transform
   (even translateY(0)) makes the wrapper a containing block + stacking context, which
   trapped position:fixed MudBlazor picker dialogs UNDER the backdrop overlay (the
   "calendar opens but only dims the screen" bug on /dashboard/analytics). */
.page-enter {
    animation: pageEnter 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}

@keyframes pageEnter {
    from {
        opacity: 0;
        transform: translateY(12px);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

/* Stagger children animation.
   PWA step 4, decision 7: „stavke liste ulaze sa pomakom … do 6”. This class already existed and
   is REUSED rather than duplicated — a second stagger class would be two definitions of one
   behaviour, and the step-2 outlet already knows to suppress this one while it is animating
   (see `.bk-route[class*="--enter"] .stagger-enter > *` further down).

   ⚠️ THE SEVENTH CHILD ONWARD CARRIES NO DELAY AT ALL — the base rule still fades it in, it just
   starts at 0ms, so a long list arrives as "the first six, then the rest". Extending the ladder is
   the obvious "improvement" and is wrong: the whole point of the cap is that a fifty-row list must
   not spend two seconds arriving.

   The steps were 50ms and are now 30ms, so the whole run finishes in 180ms rather than 300ms —
   decision 7's number, and short enough that the last row is not visibly late behind the first. */
.stagger-enter > * {
    opacity: 0;
    animation: staggerItem 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}

.stagger-enter > *:nth-child(1) { animation-delay: 0.03s; }
.stagger-enter > *:nth-child(2) { animation-delay: 0.06s; }
.stagger-enter > *:nth-child(3) { animation-delay: 0.09s; }
.stagger-enter > *:nth-child(4) { animation-delay: 0.12s; }
.stagger-enter > *:nth-child(5) { animation-delay: 0.15s; }
.stagger-enter > *:nth-child(6) { animation-delay: 0.18s; }

@keyframes staggerItem {
    from {
        opacity: 0;
        transform: translateY(8px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Button press feedback: merged into the single .mud-button-root:active rule above (2026-11-03 apple-design s1). */

/* Status chip entrance */

@keyframes chipPop {
    from { transform: scale(0.8); opacity: 0; }
    to { transform: scale(1); opacity: 1; }
}

/* Sidebar nav item hover glow */
.mud-drawer .mud-nav-link:hover {
    transition: background-color var(--motion-fast) var(--ease-standard),
                color var(--motion-fast) var(--ease-standard) !important;
}

/* No table-row entrance (audit 2026-10-15 APP-17 / MOT-23). Rows used to stagger in on every
   render — paging, sort, every keystroke of a search — and the `forwards` fill left a transform
   on every desktop row, making each one a stacking context. Motion here carries no state. Do not
   bring it back. */

/* Dialog entrance — desktop: scale, mobile: slide-up */
.mud-dialog {
    animation: dialogEnter 0.25s cubic-bezier(0.4, 0, 0.2, 1) !important;
}

@keyframes dialogEnter {
    from { opacity: 0; transform: scale(0.95) translateY(8px); }
    to { opacity: 1; transform: scale(1) translateY(0); }
}

@keyframes dialogSlideUp {
    from { transform: translateY(100%); }
    to { transform: translateY(0); }
}

@keyframes overlayFadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}

/* Snackbar entrance */
.mud-snackbar {
    animation: snackEnter 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
}

@keyframes snackEnter {
    from { opacity: 0; transform: translateY(16px) scale(0.95); }
    to { opacity: 1; transform: translateY(0) scale(1); }
}

/* Smooth skeleton pulse (override default) */
.mud-skeleton {
    animation: skeletonPulse 1.5s ease-in-out infinite !important;
}

/* ONE loading motion per placeholder (audit 2026-10-15 MOT-24): the pulse above overrides the
   element's animation, but MudBlazor's Animation.Wave sweeps on ::after and kept running, so a
   skeleton pulsed and swept at once. */
.mud-skeleton::after {
    animation: none !important;
    display: none;
}

@keyframes skeletonPulse {
    0%, 100% { opacity: 0.4; }
    50% { opacity: 0.8; }
}

/* Filled-button lift and avatar scale on hover, under a real pointer only (audit 2026-10-15
   APP-17 / MOT-21). On touch :hover latches after a tap, so the effect stayed on. The lift is a
   neutral shadow, not a --primary glow: a red confirm button glowed purple. */
@media (hover: hover) and (pointer: fine) {
    .mud-button-filled:hover {
        box-shadow: var(--shadow-md) !important;
    }

    .mud-avatar:hover {
        transform: scale(1.05);
    }
}

/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
    .page-enter,
    .stagger-enter > *,
    .count-up,
    .chip-enter {
        animation: none !important;
        opacity: 1 !important;
        transform: none !important;
    }
}

/* ============================================================
   Mobile Responsiveness — Consolidated
   ============================================================ */

/* ── Table → Card on mobile ── */
@media (max-width: 599px) {
    .mud-table .mud-table-head {
        display: none !important;
    }

    .mud-table .mud-table-body {
        display: flex !important;
        flex-direction: column !important;
        gap: var(--space-3) !important;
        padding: var(--space-2) !important;
    }

    .mud-table .mud-table-body .mud-table-row {
        display: flex !important;
        flex-wrap: wrap !important;
        padding: 14px var(--space-4) !important;
        background: var(--surface) !important;
        border: 1px solid var(--border-light) !important;
        border-radius: var(--radius-md) !important;
        box-shadow: var(--shadow-xs);
        animation: none !important;
        opacity: 1 !important;
    }

    .mud-table .mud-table-body .mud-table-row:hover {
        background: var(--surface) !important;
    }

    .mud-table .mud-table-body .mud-table-row .mud-table-cell {
        display: flex !important;
        justify-content: space-between !important;
        align-items: center !important;
        width: 100% !important;
        padding: 5px 0 !important;
        border-bottom: none !important;
        font-size: 0.85rem !important;
    }

    .mud-table .mud-table-body .mud-table-row .mud-table-cell[data-label]::before {
        content: attr(data-label);
        font-weight: 500;
        color: var(--text-muted);
        font-size: 0.8rem;
        margin-right: var(--space-3);
        flex-shrink: 0;
    }

    .mud-table .mud-table-body .mud-table-row .mud-table-cell:first-child {
        font-weight: 600 !important;
        font-size: 0.92rem !important;
        color: var(--text) !important;
        padding-bottom: var(--space-2) !important;
        border-bottom: 1px solid var(--border-light) !important;
        margin-bottom: var(--space-1) !important;
    }

    .mud-table .mud-table-footer {
        border-top: none !important;
    }

    .mud-table .mud-table-pagination {
        border-top: none !important;
        justify-content: center !important;
    }
}

/* ── Phone global fixes (< 600px) ── */
@media (max-width: 599px) {
    /* Prevent horizontal overflow */
    body {
        overflow-x: hidden;
    }

    /* Hide scrollbars on touch devices */
    ::-webkit-scrollbar {
        display: none !important;
    }
    * {
        scrollbar-width: none !important;
    }

    /* Container padding - tighter on mobile */
    .dashboard-content-area {
        padding: 14px !important;
    }

    /* Font scaling */
    .mud-typography-h4 { font-size: 1.35rem !important; }
    .mud-typography-h5 { font-size: 1.15rem !important; }
    .mud-typography-h6 { font-size: var(--text-md) !important; }

    /* Grid gaps - tighter */
    .mud-grid { margin: -6px !important; }
    .mud-grid > .mud-grid-item { padding: 6px !important; }

    /* Page headers stack with full-width button */
    .page-header {
        flex-wrap: wrap !important;
        gap: var(--space-3) !important;
    }
    .page-header .mud-button-filled {
        width: 100%;
    }

    /* Stack page headers vertically */
    .d-flex.justify-space-between.align-center.flex-wrap {
        flex-direction: column;
        align-items: flex-start !important;
        gap: var(--space-3) !important;
    }

    /* Full-width buttons on mobile */
    .d-flex.justify-space-between.align-center.flex-wrap > .mud-button-filled {
        width: 100%;
    }

    /* Cards compact */
    .mud-card .mud-card-content {
        padding: 14px !important;
    }

    /* Stat cards compact */
    .stat-card {
        padding: 14px;
    }
    .stat-value {
        font-size: 1.35rem;
    }
    .stat-label {
        font-size: 0.7rem;
    }

    /* MudPaper sections: reduce padding */
    .mud-paper {
        padding: var(--space-4);
    }

    /* Filter rows stack vertically */
    .d-flex.flex-wrap.gap-3 {
        flex-direction: column;
    }

    /* Quick action buttons full width */
    .d-flex.flex-wrap.gap-3 > .mud-button-outlined {
        width: 100%;
        justify-content: flex-start;
    }

    /* Dialog on mobile — full-screen with slide-up animation */
    .mud-dialog {
        max-height: 100dvh !important;
        height: 100dvh !important;
        max-width: 100vw !important;
        width: 100vw !important;
        margin: 0 !important;
        border-radius: 0 !important;
        display: flex !important;
        flex-direction: column !important;
        animation: dialogSlideUp 0.3s cubic-bezier(0.32, 0.72, 0, 1) !important;
    }

    .mud-dialog .mud-dialog-content {
        flex: 1 1 auto !important;
        overflow-y: auto !important;
        min-height: 0 !important;
        max-height: none !important;
        -webkit-overflow-scrolling: touch !important;
        overscroll-behavior-y: contain !important;
    }

    .mud-dialog .mud-dialog-title {
        flex: 0 0 auto !important;
        padding-top: max(16px, env(safe-area-inset-top)) !important;
    }

    .mud-dialog .mud-dialog-actions {
        flex: 0 0 auto !important;
        padding-bottom: max(16px, env(safe-area-inset-bottom)) !important;
    }

    .mud-dialog-container:not(.mud-dialog-bottomcenter) {
        align-items: stretch !important;
    }

    /* ── Bottom sheet (PWA step 2, decision 4) ──
       „bottom sheet za dijaloge na telefonu (prevlačenje nadole zatvara)”. Everything opened
       through ShowResponsiveAsync WITHOUT fullScreen: true lands here; the long forms opt back
       into the full-screen block above. DialogExtensions.cs carries the full list of those —
       do not restate it here, or the two copies drift.

       ⚠️ Specificity, not order, is what makes this win: the block above sets every one of these
       properties with !important at (0,1,0), so an override has to be !important AND more
       specific. (0,3,0) here.

       .confirm-bottom-sheet is excluded from the SIZE rules on purpose — it is already a sheet,
       deliberately capped at 50dvh, and has sanctioned users (docs/agents/core-ui/mobile-touch.md names them). Letting those reach it would silently retune a shape that was
       chosen for a reason. It is NOT excluded from the anchoring below. */
    .mud-dialog-container.mud-dialog-bottomcenter {
        align-items: flex-end !important;
        padding-bottom: 0 !important;
    }

    /* ⚠️ A .confirm-bottom-sheet can arrive through TWO doors and only one of them sets a position.
       ShowResponsiveAsync asks for BottomCenter; ShowConfirmAsync / ShowPickAsync build their own
       DialogOptions (DialogServiceExtensions.cs) — those now ask for it too, but a third caller
       that forgets would land here with .mud-dialog-center and the container's `align-items:
       stretch` above, which stretches an auto-height dialog from the TOP of the screen. A sheet
       named `-bottom-` with rounded TOP corners rendered at the top is exactly the bug this class
       was added to fix, so the anchoring is restated for the class itself rather than left to
       depend on how it was opened. */
    .confirm-bottom-sheet {
        align-self: flex-end !important;
    }

    .mud-dialog-container.mud-dialog-bottomcenter .mud-dialog:not(.confirm-bottom-sheet):not(.session-expired-dialog) {
        height: auto !important;
        /* dvh first-class, vh as the floor: a browser without dvh drops the whole declaration and
           the sheet inherits the full-screen block's `max-height: 100dvh` — which that browser also
           drops, leaving an uncapped sheet that can run off the bottom of the screen. The vh line
           is what makes the failure "a few percent taller" instead. */
        max-height: 92vh !important;
        max-height: 92dvh !important;
        border-radius: var(--radius-lg) var(--radius-lg) 0 0 !important;
        animation: bkSheetUp var(--motion-base) var(--ease-decelerate) !important;
    }

    /* The grab handle is DRAWN, not marked up. ShowResponsiveAsync opens a dialog it does not
       own the markup of, so there is nowhere to put an element — and adding one to all 89
       <MudDialog> tags is the kind of change that lands in 88 of them. As a flex item of the
       column the block above already establishes, ::before sits above the title on its own row. */
    .mud-dialog-container.mud-dialog-bottomcenter .mud-dialog:not(.confirm-bottom-sheet):not(.session-expired-dialog)::before {
        content: '';
        flex: 0 0 auto;
        width: 36px;
        height: 4px;
        margin: var(--space-2) auto 0;
        border-radius: var(--radius-full);
        background: var(--border);
    }

    /* The full-screen block pads the title by env(safe-area-inset-top) because there it IS the top
       of the screen. A sheet starts partway down, so that padding is a gap under the handle. */
    .mud-dialog-container.mud-dialog-bottomcenter .mud-dialog:not(.confirm-bottom-sheet):not(.session-expired-dialog) .mud-dialog-title {
        padding-top: var(--space-2) !important;
    }

    @keyframes bkSheetUp {
        from { transform: translate3d(0, 100%, 0); }
        to { transform: none; }
    }

    /* Dialog backdrop fade-in on mobile — dialog backdrops ONLY (.mud-overlay-dialog).
       A MudSelect inside a dialog opens a modal popover whose bare .mud-overlay
       click-catcher sits above the dialog — an opaque var(--surface) background on
       all .mud-overlay whites out the whole dialog, leaving only the floating
       options panel visible in the corner. */
    .mud-overlay.mud-overlay-dialog {
        background: var(--surface) !important;
        backdrop-filter: none !important;
        -webkit-backdrop-filter: none !important;
        animation: overlayFadeIn 0.2s ease-out !important;
    }

    /* UI audit 2026-10-15, APP-15: the opaque surface above suits a FULL-SCREEN dialog, which
       covers it anyway. A bottom sheet does not cover it, so the sheet sat on a ground the same
       colour as itself: the grab handle floated on a blank page and the calendar behind the
       termin was gone. Sheets get the scrim instead. Still scoped to the dialog overlay, never
       the bare .mud-overlay (a select inside the sheet opens one above it). (0,4,0) beats the
       (0,2,0) rule above; both are !important. */
    .mud-dialog-container.mud-dialog-bottomcenter .mud-overlay.mud-overlay-dialog {
        background: var(--overlay-scrim) !important;
    }

    /* The row-actions sheet (Mobbin wave 2, B1/B3): the phone's "⋯" on a klijent or zaposleni
       row. Rendered by src/Escalybook.Core.Ui/Components/Shared/RowActionsSheet.razor; it shares
       .confirm-bottom-sheet below, so it is a half-height sheet, never a full-screen page. That
       holds for the short variant only: the tall variant (faktura panel, `Tall`) does not carry
       the class (docs/agents/core-ui/mobile-touch.md). Each
       action is a 48px full-width text button; the destructive one takes the error INK, not the
       hue (APP-14), and the caller puts it last. */
    .row-actions-sheet .ras-body {
        padding: var(--space-3) var(--space-2) var(--space-4);
    }

    .row-actions-sheet .ras-title {
        font-weight: 600;
        font-size: var(--text-md);
        color: var(--text);
        padding: 0 var(--space-4);
        overflow-wrap: anywhere;
    }

    .row-actions-sheet .ras-subtitle {
        font-size: var(--text-sm);
        color: var(--text-muted);
        padding: 0 var(--space-4);
        overflow-wrap: anywhere;
    }

    .row-actions-sheet .ras-details {
        padding: 0 var(--space-4);
        margin-top: var(--space-2);
    }

    .row-actions-sheet .ras-details-note {
        margin-top: var(--space-2);
        font-size: var(--text-sm);
        color: var(--text-muted);
        overflow-wrap: anywhere;
    }

    .row-actions-sheet .ras-list {
        display: flex;
        flex-direction: column;
        gap: var(--space-1);
        margin-top: var(--space-3);
    }

    .row-actions-sheet .ras-item {
        justify-content: flex-start;
        text-transform: none;
        min-height: 48px;
        padding: var(--space-3) var(--space-4);
        border-radius: var(--radius-sm);
        color: var(--text);
        font-weight: 500;
    }

    .row-actions-sheet .ras-item--danger {
        color: var(--error-text);
    }

    /* Confirm/delete dialogs — bottom sheet, not full-screen */
    .confirm-bottom-sheet {
        height: auto !important;
        max-height: 50vh !important;
        max-height: 50dvh !important;
        border-radius: var(--radius-lg) var(--radius-lg) 0 0 !important;
        animation: dialogSlideUp 0.25s cubic-bezier(0.32, 0.72, 0, 1) !important;
    }

    /* CONTRACT: ConfirmDialog in bookly.core.ui emits confirm-stacked-actions when ConfirmColor is
       Error. Nothing spans the two repos to check that — a rename there silently stops this rule
       matching, with a green build on both sides, which is why core.ui pins the exact class list in
       a test. Destructive confirms only: an ordinary one keeps the compact right-aligned row.

       ConfirmDialog.razor renders Cancel FIRST in the DOM and Confirm second, and column-reverse
       is what turns that into Confirm ABOVE Cancel on screen — so the destructive button sits away
       from the thumb while tab order and screen-reader order still reach Cancel first. The order
       in that file is load-bearing: swap the two <MudButton>s and this rule puts the red button in
       the thumb zone AND makes Tab land on it first, with green builds and green bUnit tests on
       both repos, because bUnit applies no CSS.

       Specificity (0,2,0) beats MudBlazor's own (0,1,0) without !important. padding-bottom is
       deliberately NOT set: .mud-dialog .mud-dialog-actions above already applies
       max(16px, env(safe-area-inset-bottom)) !important and would win regardless. */
    .confirm-stacked-actions .mud-dialog-actions {
        flex-direction: column-reverse;
        align-items: stretch;
        justify-content: flex-start;
        gap: 10px;
        padding-left: var(--space-4);
        padding-right: var(--space-4);
    }

    .confirm-stacked-actions .mud-dialog-actions .mud-button {
        width: 100%;
        /* !important, and not because of MudBlazor. This file's own
           `.mud-button-root { min-height: 44px !important }` under
           `(hover: none) and (pointer: coarse)` sits LATER and wins on !important regardless of
           this rule's higher specificity — so on a phone, the one device this targets, the buttons
           rendered 44px and the declaration did nothing at all. The identical trap is already
           documented against this file at the `.mud-input .mud-icon-button` carve-out above, which
           "had NO effect anywhere before it was given !important". */
        min-height: 52px !important;
    }

    /* Touch feedback on interactive elements */
    .mud-card:active,
    .card-interactive:active,
    .tenant-card:active,
    .service-card:active,
    .employee-card:active,
    .mud-list-item:active {
        transform: scale(0.98) !important;
        opacity: 0.9 !important;
        transition: transform 100ms ease-out, opacity 100ms ease-out !important;
    }

    /* 2026-11-03 apple-design s1: the release is the eased one. The press rule above only lives
       while the finger is down, so without a base transition the card snapped back. */
    .mud-card,
    .card-interactive,
    .tenant-card,
    .employee-card,
    .mud-list-item {
        /* Background and shadow stay listed: this replaces .mud-card's own transition list above and
           MudBlazor's list-item one. .service-card already carries a transform transition (line ~941). */
        transition: transform 160ms cubic-bezier(.32, .72, 0, 1),
                    opacity 160ms cubic-bezier(.32, .72, 0, 1),
                    background-color var(--motion-fast) var(--ease-standard),
                    color var(--motion-fast) var(--ease-standard),
                    border-color var(--motion-fast) var(--ease-standard),
                    box-shadow var(--motion-fast) var(--ease-standard);
    }

    /* Login page: full width form */
    .login-form-card {
        padding: var(--space-6) var(--space-4);
    }

    /* Booking wizard: tighter wizard container */

    /* Time slot grid: 3 columns on mobile */
    .wizard-calendar-wrapper + div > div[style*="grid-template-columns"] {
        grid-template-columns: repeat(3, 1fr) !important;
    }

    /* Appbar: tighter on mobile */
    .mud-appbar .mud-toolbar {
        padding: 0 var(--space-2) !important;
    }

    /* Hide less important table columns */
    .hide-mobile {
        display: none !important;
    }

    /* Bottom safe area for iOS — main padding handled by DashboardLayout scoped styles */
    .mud-main-content {
        padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));
    }

    /* Expansion panels */
    .mud-expand-panel .mud-expand-panel-header {
        padding: var(--space-3) var(--space-4) !important;
    }

    /* Reduce section spacing */
    .section {
        padding: var(--space-10) 0;
    }
}

/* ── Mobile / Desktop visibility utilities ── */

/* ── Tablet adjustments (600-959px) ── */
@media (min-width: 600px) and (max-width: 959px) {
    .mud-typography-h4 {
        font-size: var(--text-3xl) !important;
    }

    /* Two-column stat cards */
    .mud-grid > .mud-grid-item-xs-12.mud-grid-item-sm-6.mud-grid-item-md-3 {
        max-width: 50%;
        flex-basis: 50%;
    }
}

/* ── Touch-friendly improvements ── */
@media (hover: none) and (pointer: coarse) {
    /* Larger touch targets */
    .mud-nav-link {
        min-height: 48px !important;
    }

    .mud-button-root {
        min-height: 44px !important;
    }

    .mud-icon-button {
        min-width: 44px !important;
        min-height: 44px !important;
    }

    /* ── CARVE-OUT: calendar day cells ──────────────────────────────────────────────────
       A date-picker day is a mud-icon-button, so the 44px rule above applied to it — and it
       BROKE the picker outright on every touch device (reported from production 2026-08-18:
       "avgust 27. se ne vidi", the tenant could not select a day off at all).

       MudBlazor sizes a week as seven .mud-day cells of 36px + 2px side margins = 280px, laid
       out by .mud-picker-calendar-week, which is `display:flex` with NO flex-wrap. Forcing
       min-width:44px makes each cell 48px and the row 336px, so the seventh column is simply
       CLIPPED — every row showed six days and the tail of the month (26–31 in August 2026)
       was unreachable. Worse, .mud-day-label in the header is NOT a button and kept its 36px,
       so the weekday headings drifted out of alignment with the numbers under them.

       The days stay at MudBlazor's own 36px. That is under the 44px touch guideline, and it is
       the right trade here: these are grid-adjacent targets, and a calendar you cannot use at
       all is worse than one whose cells are 8px small. Pinned by CalendarTouchTargetGuardTests
       so the general rule cannot silently swallow them again.

       Scoped to .mud-picker-calendar-week, NOT .mud-picker-calendar-content. The month prev/next
       arrows live in .mud-picker-calendar-header-switch, which is also inside -content, and they
       are ordinary standalone buttons with no clipping problem — the wider scope was quietly
       dropping them from 44px to their natural 40px for no benefit. The week row is the only
       flex container that overflows, so it is the only one that needs the exemption. */
    .mud-picker .mud-day,
    .mud-picker-calendar-week .mud-icon-button {
        min-width: 36px !important;
        min-height: 36px !important;
    }

    /* ── CARVE-OUT: the switch thumb ──────────────────────────────────────────────────
       Reported from production 2026-08-24: "toggle za primam termine pokvaren" — the thumb
       had fallen out of the bottom of its track.

       Same mechanism as the calendar cells above, and the second time this rule has broken a
       component by growing it. MudBlazor builds a switch as a fixed-size .mud-switch-span
       (44x24 small, 58x38 medium) with overflow:hidden, containing an ABSOLUTELY positioned
       .mud-switch-base at top:0 left:0 — and that base is a .mud-icon-button, so the floor
       above inflated it to 44x44. Because the base is absolute, growing it does not grow the
       track: it just repositions the thumb inside it. Measured in Chromium against this very
       stylesheet with the coarse block forced on:

         small   thumb 10px BELOW the track centre, 12px past its right end, 5px of the
                 circle clipped away by the span's overflow:hidden
         medium  thumb 3px below, 6px past the right end

       So the switch was visibly broken on every touch device, on every page — the employees
       page is just where it was noticed. Checkbox and radio were MEASURED and are fine: their
       button is position:relative, so the floor grows the box and the icon stays centred.
       That is the whole class — position:absolute inside a fixed-size overflow:hidden parent.

       ⚠️ This does NOT give the switch a smaller tap target, and must not be "fixed" by
       putting the floor back. The element a tap actually activates is the LABEL: .mud-switch
       carries cursor:pointer and wraps both the track and its text, and clicking a label
       toggles the input inside it. So the floor moves DOWN to the label, one rule below, where
       it enlarges the real hit area instead of the decoration. The base's own 24px was never
       the target in the first place.

       Specificity is (0,2,0) — deliberately above the (0,1,0) floor rather than relying on
       source order, since both carry !important and order is the only other tie-breaker.
       Pinned by SwitchTouchTargetGuardTests. */
    .mud-switch .mud-switch-base {
        min-width: 0 !important;
        min-height: 0 !important;
    }

    /* The floor, moved to the element that receives the tap.

       The `label` in the selector is load-bearing, not decoration. MudBlazor puts the
       .mud-switch class on TWO elements: the <label> wrapper AND the <span> holding the label
       text. A bare `.mud-switch` would floor both, giving the text its own 44px band inside an
       already-44px label. Naming the tag keeps it on the wrapper — measured: with this rule the
       text span computes min-height:auto.

       Verified in Chromium for both shapes a switch comes in. With a label the hit area is the
       full row, 44px tall; WITHOUT one — the label wraps only the 44x24 track — it is 44x44, so
       releasing the base above never shrinks a target below the floor. */
    label.mud-switch {
        min-height: 44px !important;
    }

    /* Remove hover effects that look stuck on touch */
    .card-interactive:hover,
    .tenant-card:hover,
    .service-card:hover,
    .employee-card:hover {
        transform: none !important;
    }

    /* Disable hover shadows on touch */
    .mud-card:hover {
        box-shadow: var(--shadow-sm) !important;
        transform: none !important;
    }
}

/* ── Landscape phone ── */
@media (max-height: 500px) and (orientation: landscape) {
    .login-page-centered {
        min-height: auto;
        padding: var(--space-4) var(--space-6);
        align-items: flex-start;
    }

    .login-form-card {
        padding: var(--space-4) var(--space-5) !important;
    }

    .login-form-card .mb-1 {
        margin-bottom: var(--space-1) !important;
    }

    .login-form-card .mb-8 {
        margin-bottom: var(--space-3) !important;
    }

    .login-form-card .mt-8 {
        margin-top: var(--space-3) !important;
    }

    .login-submit-btn {
        margin-top: var(--space-4) !important;
    }
}

/* ── Customer/Portal mobile polish ── */
@media (max-width: 599px) {
    /* Tighter card padding on customer pages */
    .customer-main-content .mud-paper {
        padding: var(--space-4) !important;
    }

    /* Profile avatar card: smaller avatar on mobile */
    .customer-main-content .mud-avatar {
        width: 52px !important;
        height: 52px !important;
        font-size: 1.1rem !important;
    }

    /* Reduce h3 heading on MyBookings */
    .mb-header .mud-typography-h3 {
        font-size: var(--text-3xl) !important;
    }

    /* Tighter bottom margin for page headers */
    .mb-6 {
        margin-bottom: var(--space-4) !important;
    }

    /* Full-width save/action buttons on mobile */
    .customer-main-content .d-flex.justify-end .mud-button-filled {
        width: 100%;
    }

    .customer-main-content .d-flex.justify-end {
        justify-content: stretch !important;
    }
}

/* ── Enforce 44px touch targets on mobile ── */
@media (max-width: 599px) {
    /* MudBlazor v9 emits `mud-icon-button-size-small`, not `mud-button-size-small`.
       This selector was written with the latter and therefore never matched a single
       element — the rule has been dead since it was added, leaving every small icon
       button (the row actions on the customers/services/employees/bookings lists,
       ~33 of them) at ~25px on narrow screens.
       Real touch devices were still covered by the (pointer: coarse) block above,
       which sizes ALL icon buttons; this rule is what covers narrow non-touch and
       anything the coarse-pointer query misses. Both spellings are matched so a
       future MudBlazor rename cannot silently kill it again. */
    .mud-icon-button.mud-icon-button-size-small,
    .mud-icon-button.mud-button-size-small {
        min-width: 44px;
        min-height: 44px;
    }

    footer a, .mud-link {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
    }
}

/* ── Print cleanup ── */
/* "U goloj šemi" (fitness part 6): what prints is the content. The chrome, the bottom bar, dialogs and
   anything a page marks .no-print go; a page's .print-only block (the karton's findings list, the
   program as text) appears only on paper. Controls are NOT hidden wholesale — a MudBlazor input's
   VALUE renders inside .mud-input-control, and hiding that printed the program blank (#495 review). */
.print-only { display: none; }
@media print {
    .mud-appbar, .mud-drawer, .mud-snackbar-provider,
    .mud-fab, .language-switcher, #blazor-error-ui,
    .no-print, .mobile-bottom-nav, .mud-dialog-container, .mud-overlay {
        display: none !important;
    }

    .print-only { display: block !important; }
    .page-enter, .stagger-enter, .stagger-enter > * { animation: none !important; opacity: 1 !important; }

    .mud-main-content {
        padding: 0 !important;
        margin: 0 !important;
    }

    body {
        font-size: 12pt;
        color: #000;
        background: #fff;
    }

    /* The paper shapes of the two bare views. */
    .pb-print-week, .dc-print-title { font-weight: 700; margin: 12pt 0 4pt; }
    .pb-print-day { font-weight: 600; margin: 8pt 0 2pt; }
    .pb-print-note { font-style: italic; margin: 0 0 4pt; }
    .pb-print ol, .dc-print ul { margin: 0 0 6pt 18pt; }
}

/* Weekly calendar mobile — global for reliability (Blazor WASM inline styles may load late) */
.wc-mobile { display: none; }
.wc-mobile-selector { display: none; }
@media (max-width: 959px) {
    .wc-desktop { display: none !important; }
    /* MUST be flex column — otherwise the inner .wc-scroll-area's flex:1 silently degrades
       to natural content height, making scrollHeight === clientHeight and breaking mobile
       vertical scroll entirely. See Playwright instrumentation 2026-05-17 confirming this
       was the actual root cause that PRs #65/#66/#70 misdiagnosed as a touch-event trap. */
    .wc-mobile { display: flex !important; flex-direction: column !important; }
    .wc-mobile-selector { display: block !important; }
}

/* ── Stacked table cards (.bl-table-cards) ────────────────────────────────────────────────────
   A MudSimpleTable turns into one card per row below 600px, with each cell labelled from its
   data-label.

   ⚠️ DEFINED HERE, ONCE, and that is a correction rather than a preference. These rules lived only
   in page-local <style> blocks — four byte-identical copies plus two more embedded in larger blocks
   — so a page that used the class without remembering to paste the CSS got a five-column desktop
   table on a phone and its data-label attributes did nothing at all. That is exactly how ten of the
   product's twenty simple tables came to believe they stacked when none of them did, and the karton
   index reproduced it on its first day. A class whose behaviour depends on remembering to paste
   forty lines is not a class. (Codex gate, blazor#428.)

   Two page-local blocks still repeat these rules alongside CSS of their own
   (AdvancedAnalyticsSection, TenantDetail). They are now redundant rather than load-bearing —
   identical declarations at identical specificity — and were left in place because untangling them
   from their neighbours is a separate change with its own risk. */
@media (max-width: 599.98px) {
    /* Cards are full-width blocks, so the container's own scroller has nothing left to scroll.
       Both axes, because overflow-x alone cannot be `visible` while overflow-y is `auto`. */
    .bl-table-cards .mud-table-container { overflow: visible; }
    .bl-table-cards table { display: block !important; }
    .bl-table-cards thead { display: none !important; }
    .bl-table-cards tbody { display: block !important; }

    .bl-table-cards tbody tr {
        display: block !important;
        background: var(--surface);
        border: 1px solid var(--border-light);
        border-radius: var(--radius-md);
        padding: var(--space-1) var(--space-3);
        margin-bottom: 10px;
    }

    .bl-table-cards tbody td {
        display: flex !important;
        align-items: center;
        justify-content: space-between;
        gap: var(--space-3);
        text-align: right !important;
        border-bottom: none;
        padding: 7px 0 !important;
    }

    .bl-table-cards tbody td[data-label]::before {
        content: attr(data-label);
        flex: 0 0 auto;
        text-align: left;
        font-size: 0.68rem;
        font-weight: 600;
        color: var(--text-muted);
    }

    /* The empty-state row spans the table and has no label to pair with. */
    .bl-table-cards tbody td[colspan] { display: block !important; text-align: center !important; }

    /* Action buttons keep their own row-end alignment and wrap rather than overflow the card. */
    .bl-table-cards tbody td > .d-flex { flex-wrap: wrap; justify-content: flex-end; }
}

/* ============================================================
   PWA foundation — step 1 (decisions 3 and 5, 2026-09-07)
   ------------------------------------------------------------
   Skeletons, the navigation progress bar and the busy-button slot.

   These live HERE rather than in each component's own <style> block for two reasons. They are
   design-system primitives shared by three components and a dozen pages, so a per-component copy
   is three copies of one shimmer that drift; and `StyledButNeverRenderedGuardTests` only scans a
   component's FIRST <style> block, so a second block in ListSkeleton would be unguarded anyway.

   Everything here is built on --motion-* / --ease-* and is zeroed by the reduced-motion block at
   the bottom of this section.
   ============================================================ */

/* ── Skeleton primitives ── */
.bk-skel {
    background: linear-gradient(90deg, var(--skeleton-base) 0%, var(--skeleton-shine) 50%, var(--skeleton-base) 100%);
    background-size: 200% 100%;
    animation: bk-skel-shimmer 1.4s ease-in-out infinite;
    border-radius: var(--radius-sm);
}

@keyframes bk-skel-shimmer {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

.bk-skel-avatar {
    width: 36px;
    height: 36px;
    border-radius: var(--radius-full);
    flex-shrink: 0;
}

.bk-skel-line {
    height: 12px;
    border-radius: 4px;
}

.bk-skel-line--lg { height: 16px; }
.bk-skel-line--sm { height: 10px; }

/* Rows and cards. A skeleton's job is to hold the shape the real content will take, so these
   mirror the paddings of .card-responsive and the dashboard list rows rather than inventing
   their own — a placeholder that is the wrong height moves the page when data lands, which is
   the jank the skeleton was added to remove. */
.bk-skel-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--border-light);
}

.bk-skel-row__body {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-width: 0;
}

.bk-skel-card {
    padding: var(--space-4);
    background: var(--surface);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-md);
    margin-bottom: var(--space-2);
}

.bk-skel-card__head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}

.bk-skel-tiles {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
    gap: var(--space-3);
}

.bk-skel-stat {
    padding: var(--space-4);
    background: var(--surface);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-md);
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* Table variant — a header strip plus rows of even cells, so a desktop table does not collapse
   to a stack of cards for the second and a half it is loading. */
.bk-skel-table {
    border: 1px solid var(--border-light);
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--surface);
}

.bk-skel-table__row {
    display: grid;
    gap: var(--space-3);
    align-items: center;
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--border-light);
}

.bk-skel-table__row:last-child { border-bottom: none; }

.bk-skel-table__row--head { background: var(--bg); }

/* Week grid — seven columns of stacked blocks under a day-name strip. */
.bk-skel-cal {
    border: 1px solid var(--border-light);
    border-radius: var(--radius-md);
    background: var(--surface);
    padding: var(--space-3);
    overflow: hidden;
}

.bk-skel-cal__head,
.bk-skel-cal__grid {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 6px;
}

.bk-skel-cal__head { margin-bottom: 10px; }

.bk-skel-cal__col {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

/* ── Navigation progress bar (decision 3) ──
   Fixed under the app bar, 3px, indeterminate. It is deliberately NOT a MudProgressLinear: it
   must paint before the target page's first render, including inside a circuit where that render
   is a round trip, and it must not participate in MudBlazor's own layout. */
.bk-navprogress {
    position: fixed;
    top: env(safe-area-inset-top, 0px);
    left: 0;
    right: 0;
    height: 3px;
    /* Between MudBlazor's app bar (1100) and its dialogs/popovers (1400). At 2000 the bar drew a
       purple line ACROSS an open dialog and its backdrop - a navigation cannot start from behind a
       modal anyway, so nothing is lost by sitting under one. */
    z-index: 1300;
    pointer-events: none;
    opacity: 0;
    transition: opacity var(--motion-fast) var(--ease-standard);
    overflow: hidden;
    background: var(--primary-light);
}

.bk-navprogress--active { opacity: 1; }

.bk-navprogress__bar {
    display: block;
    height: 100%;
    width: 40%;
    background: var(--primary);
    border-radius: var(--radius-full);
    transform: translateX(-100%);
}

.bk-navprogress--active .bk-navprogress__bar {
    animation: bk-navprogress-slide 1.1s var(--ease-standard) infinite;
}

@keyframes bk-navprogress-slide {
    0% { transform: translateX(-100%); }
    60% { transform: translateX(160%); }
    100% { transform: translateX(260%); }
}

/* ── Busy button (decision 3: the spinner lives in the pressed button, nowhere else) ──
   The leading slot is ALWAYS rendered, idle included. That is what keeps the button from
   growing by ~28px the moment it is pressed — reserving the space only while busy is the very
   jump the reserve exists to prevent, and a primary submit button that resizes under a thumb
   moves the thing being tapped. */
.bk-busy-btn__slot {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    margin-inline-end: var(--space-2);
    flex-shrink: 0;
}

.bk-busy-btn__slot .mud-progress-circular {
    width: 18px;
    height: 18px;
    color: inherit;
}

.bk-busy-btn__label {
    display: inline-flex;
    align-items: center;
    min-width: 0;
}

/* ── Page transitions (PWA step 2, decision 2) ──
   „klizanje levo/desno po hijerarhiji (lista → detalj), fade za tabove, nazad = obrnuto”.

   ⚠️ ENTER ONLY, AND NO `forwards` FILL — the same constraint .page-enter above carries a comment
   about, and for the same reason: a RETAINED transform on a content wrapper makes it a containing
   block and a stacking context, which trapped position:fixed MudBlazor pickers under the backdrop
   ("the calendar opens but only dims the screen"). The class is added by JS on the render that
   follows the navigation and removed on animationend, so the resting state is untouched. Animating
   the OUTGOING page would need exactly the retained transform this forbids, and would have to hold
   it for a whole circuit round trip. */
.bk-route {
    /* No transform, no opacity, no containment in the resting state. See above. */
    min-width: 0;
    /* ⚠️ `clip`, NOT `hidden`, and the difference is load-bearing twice over. The enter animation
       starts the page 24px to one side, which on a desktop viewport can hand the document a
       horizontal scrollbar for the length of the animation — `body { overflow-x: hidden }` only
       exists below 600px, and PartnerLayout has no container padding to absorb it at all.
       `hidden` would create a SCROLL CONTAINER, and the twelve `position: sticky` elements inside
       pages would then stick to this box instead of the viewport; `clip` creates none, so they
       keep working. It also does not force the other axis, so vertical layout is untouched, and it
       establishes no containing block, so it is not the position:fixed trap the comment above
       warns about.
       Checked before adding it: no page inside this outlet breaks out of its container. Every
       horizontal negative margin in Pages/ and Components/ is either a ≤8px nudge or belongs to a
       DIALOG, and dialogs render at the document root through MudDialogProvider — outside this
       element. The same is true of every `100vw` rule. */
    overflow-x: clip;
}

/* ⚠️ 64 components already carry `class="page-enter"`, a 350ms fade-up they run on their own. With
   the outlet sliding at the same time the page arrives twice, which reads as a wobble rather than
   as two animations. The outlet wins while it is animating; the class is removed on animationend
   and .page-enter is left alone from then on (it has already run inside the suppressed window, so
   nothing starts late). Deliberately the smaller diff: stripping the class from 64 files would
   also delete the animation for every navigation the outlet does not animate — a full page load,
   reduced motion, a same-route re-render. */
.bk-route[class*="--enter"] .page-enter,
.bk-route[class*="--enter"] .stagger-enter > * {
    animation: none;
}

.bk-route--enter-forward {
    animation: bkRouteEnterForward var(--motion-base) var(--ease-decelerate);
}

.bk-route--enter-backward {
    animation: bkRouteEnterBackward var(--motion-base) var(--ease-decelerate);
}

.bk-route--enter-lateral {
    animation: bkRouteEnterLateral var(--motion-fast) var(--ease-standard);
}

@keyframes bkRouteEnterForward {
    from { opacity: 0; transform: translate3d(24px, 0, 0); }
    to { opacity: 1; transform: none; }
}

@keyframes bkRouteEnterBackward {
    from { opacity: 0; transform: translate3d(-24px, 0, 0); }
    to { opacity: 1; transform: none; }
}

@keyframes bkRouteEnterLateral {
    from { opacity: 0; }
    to { opacity: 1; }
}

/* ── The View Transitions half, WASM routes only ──
   The name is applied by JS for the duration of the transition and removed again, never left on
   the element: a live view-transition-name applies containment, and containment on a content
   wrapper is the same trap as a retained transform. */
.bk-route--vt {
    view-transition-name: bk-route;
}

html[data-bk-nav="forward"]::view-transition-old(bk-route) {
    animation: bkRouteVtOutLeft var(--motion-base) var(--ease-accelerate) both;
}

html[data-bk-nav="forward"]::view-transition-new(bk-route) {
    animation: bkRouteEnterForward var(--motion-base) var(--ease-decelerate) both;
}

html[data-bk-nav="backward"]::view-transition-old(bk-route) {
    animation: bkRouteVtOutRight var(--motion-base) var(--ease-accelerate) both;
}

html[data-bk-nav="backward"]::view-transition-new(bk-route) {
    animation: bkRouteEnterBackward var(--motion-base) var(--ease-decelerate) both;
}

html[data-bk-nav="lateral"]::view-transition-old(bk-route) {
    animation: bkRouteVtFadeOut var(--motion-fast) var(--ease-standard) both;
}

html[data-bk-nav="lateral"]::view-transition-new(bk-route) {
    animation: bkRouteEnterLateral var(--motion-fast) var(--ease-standard) both;
}

@keyframes bkRouteVtOutLeft {
    from { opacity: 1; transform: none; }
    to { opacity: 0; transform: translate3d(-24px, 0, 0); }
}

@keyframes bkRouteVtOutRight {
    from { opacity: 1; transform: none; }
    to { opacity: 0; transform: translate3d(24px, 0, 0); }
}

@keyframes bkRouteVtFadeOut {
    from { opacity: 1; }
    to { opacity: 0; }
}

/* ══════════════════════════════════════════════════════════════════════════════════════════
   PWA step 3 — offline, installation, update, splash (decisions 6 and 8)
   ══════════════════════════════════════════════════════════════════════════════════════════ */

/* ── Boot splash (decision 8) ──
   The first painted frame of every document. Markup is in App.razor and it must stay free of
   Blazor: on /book/* and /dashboard/* the circuit has not connected yet, and on the WASM routes
   the runtime has not been downloaded, so this is the only thing on screen for the first second
   or two of a standalone launch. js/escalybook-boot.js is what removes it. */
.bk-splash {
    position: fixed;
    inset: 0;
    z-index: 9998;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg);
    /* Standalone runs edge to edge under the status bar and the home indicator; the padding is
       what keeps the mark off both. In a browser tab every inset is 0 and this costs nothing. */
    padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)
             env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
}

.bk-splash__inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 22px;
}

.bk-splash__logo {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 72px;
    height: 72px;
    color: var(--primary);
}

/* A firma's logo is an arbitrary image — contain it rather than letting a wide banner decide the
   splash's layout. */
.bk-splash__logo-img {
    max-width: 72px;
    max-height: 72px;
    object-fit: contain;
    border-radius: var(--radius-md);
}

.bk-splash__spinner {
    width: 26px;
    height: 26px;
    border: 3px solid var(--primary-light);
    border-top-color: var(--primary);
    border-radius: 50%;
    animation: bkSplashSpin 0.7s linear infinite;
}

@keyframes bkSplashSpin {
    to { transform: rotate(360deg); }
}

/* ── „Nema mreže" bar (decision 6) and „Nova verzija je spremna" (decision 8) ──
   Never a dialog: decision 5 already established that taking the page away from somebody mid-task
   is the wrong answer to a connectivity event, and neither of these is more urgent than a dropped
   circuit. The offline bar is a thin strip pinned under the reconnect bar's slot; the update
   notice used to be the same strip and since Mobbin wave 3 (A7) is the floating toast below.

   ⚠️ z-index sits BELOW #components-reconnect-modal (10000). A dropped circuit and a lost network
   usually happen together, and the reconnect bar is the one that is actually recovering. */
.bk-appbar-strip {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 9990;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    min-height: 34px;
    padding: 6px 14px;
    padding-top: calc(6px + var(--bk-safe-top));
    font-size: var(--text-sm);
    line-height: 1.3;
    box-shadow: var(--shadow-sm);
    border-bottom: 1px solid var(--border);
}

.bk-appbar-strip--offline {
    background: var(--warning-light);
    color: var(--warning-text);
}

/* ── „Nova verzija je spremna" toast (Mobbin wave 3, A7, 2026-10-19) ──
   NOT a strip any more: it floats, and nothing under it moves (AppUpdateBar.razor says why). It is
   not a .bk-appbar-strip, so none of the offset rules below can match it. On a phone it sits just
   above the bottom bar (60px plus the home indicator) and keeps clear of the dashboard's FAB
   (right: 20px, 56px wide); MudBlazor's own toasts sit higher still (bottom: 144px), so the two
   never overlap. On a desktop it is a pill at the bottom centre, away from MudBlazor's bottom-right
   toasts. */
.bk-update-toast {
    position: fixed;
    left: 50%;
    bottom: var(--space-6);
    transform: translateX(-50%);
    z-index: 1350;
    display: flex;
    align-items: center;
    gap: var(--space-3);
    max-width: calc(100vw - 2 * var(--space-4));
    padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-full);
    background: var(--surface-overlay);
    color: var(--text);
    box-shadow: var(--shadow-lg);
    font-size: var(--text-sm);
    line-height: 1.3;
}

@media (max-width: 959px) {
    .bk-update-toast {
        left: var(--space-4);
        right: var(--space-4);
        bottom: calc(68px + env(safe-area-inset-bottom, 0px));
        transform: none;
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-2);
        padding: var(--space-3);
        border-radius: var(--radius-md);
    }

    /* The dashboard's FAB is 56px wide at right: 20px; leave it its column. DashboardLayout says
       when the FAB is on screen (AppUpdateBar ClearsFab), so no selector has to look for it. */
    .bk-update-toast--fab {
        right: 88px;
    }
}

.bk-appbar-strip__dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: currentColor;
    flex-shrink: 0;
    opacity: 0.8;
}

.bk-update-toast__btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 26px;
    padding: 3px 14px;
    border: none;
    border-radius: var(--radius-full);
    background: var(--primary);
    color: var(--primary-btn-text);
    font-family: inherit;
    font-size: var(--text-sm);
    font-weight: 600;
    cursor: pointer;
}

/* A 44px TARGET without a 44px pill (audit 2026-10-15 APP-19). The update notice's button is the
   one action that gets a stale phone onto the new version, and at 26px it was under the touch
   floor. The pill keeps the look it had in the old 34px strip and the hit area grows instead
   (9px above and below the 26px pill is 44px). */
.bk-update-toast__btn::before {
    content: "";
    position: absolute;
    top: -9px;
    bottom: -9px;
    left: 0;
    right: 0;
}

/* The touch floor for NATIVE buttons on phone routes (audit 2026-10-15 APP-19). The floors above
   cover Mud buttons and clickable chips only. These classes are given to the plain <button>
   toggles in CustomersList (.cust-chip), WeeklyCalendar's colour-by toggle (.wc-colorby-btn) and
   NewBookingDialog's existing/new toggle (.nbd-toggle-btn). */
@media (pointer: coarse) {
    .cust-chip,
    .wc-colorby-btn,
    .nbd-toggle-btn {
        min-height: 44px;
    }
}

.bk-appbar-strip__text,
.bk-update-toast__text {
    /* A long Serbian sentence on a 320px screen must wrap rather than push the button off. */
    min-width: 0;
}

/* Owner on a phone, 2026-11-01: the press must be impossible to miss. After the base rule above, or it loses. */
@media (max-width: 959px) {
    .bk-update-toast__btn {
        width: 100%;
        min-height: 52px;
        padding: var(--space-2) var(--space-4);
        border-radius: var(--radius-md);
        font-size: 1rem;
    }

    .bk-update-toast__btn::before {
        display: none;
    }

    .bk-update-toast__text {
        text-align: center;
    }
}

/* ── Getting out of the app bar's way ──
   The offline strip is `position: fixed; top: 0`, so it would sit over the app bar, hiding the page
   title and the back/menu buttons for as long as an outage lasts. Since Mobbin wave 3 (A7) it is
   the ONLY .bk-appbar-strip: the update notice became a floating toast that moves nothing, so the
   two-strip stacking rules that used to follow here are gone.

   Solved with a sibling combinator and no JavaScript, because the five layouts all render
   `<OfflineBar /> <AppUpdateBar /> <MudLayout>` as siblings, and OfflineBar renders NOTHING (not a
   hidden element) while online, so the presence of its div in the DOM is exactly the condition
   "the strip is showing".

   ⚠️ Keep --bk-strip-h and .bk-appbar-strip's min-height in step. A strip whose text wraps on a
   very narrow screen is taller than the offset, which costs a few pixels of overlap and nothing
   else; that is the accepted limit of doing this without measuring in JS. */
:root {
    --bk-strip-h: 34px;

    /* ⚠️ THE REAL INSET, UNCLAMPED, and it must stay that way. This token feeds the strips' own
       padding and the offset ladder below, i.e. how far DOWN the app bar and the content sit — and
       clamping that is how you hide the first line of every page behind the notch on a device whose
       inset is larger than the guess. The number that made the guess tempting is 59px, the iPhone
       Dynamic Island: a 44px clamp shipped here would have regressed the offline and update bars on
       every one of those phones to fix a band nobody has reproduced outside one Android PWA.
       The clamp that the reconnect strip does need is a ceiling on ITS OWN HEIGHT, and it lives in
       App.razor with the height it bounds — one element, one bound, nothing else affected. */
    --bk-safe-top: env(safe-area-inset-top, 0px);
}

/* The strip live: push the fixed app bar and the content below it down past it.
   ⚠️ padding-top: 0 on the app bar is the standalone half. The @media (display-mode: standalone)
   block further down gives the bar its own safe-area padding so it clears the notch — but while a
   strip is on screen the STRIP is what sits under the notch (it carries the inset in its own
   padding-top), so leaving the bar's padding on would count the same inset twice and leave a band
   of background between the two. Three classes beat that block's one, so this wins wherever it
   applies, regardless of source order. */
.bk-appbar-strip ~ .mud-layout .mud-appbar {
    top: calc(var(--bk-strip-h) + var(--bk-safe-top));
    padding-top: 0;
}

.bk-appbar-strip ~ .mud-layout .mud-main-content {
    margin-top: calc(var(--bk-strip-h) + var(--bk-safe-top));
}

/* ── The reconnect strip takes the top slot too, and until 2026-09-08 nothing moved for it ──
   #components-reconnect-modal is a `position: fixed` strip at `top: 0` exactly like the two above,
   but it is declared AFTER <Routes> in App.razor, so no sibling combinator can reach the layout
   from it — which is why it was the one strip with no offset at all. It simply covered the app
   bar: the operator's screenshot shows the firma's user chip sliced in half behind „Povezujem…”,
   on a phone, several times a day.
   js/escalybook-reconnect.js puts `bk-reconnect-down` on <html> for exactly as long as the strip is
   showing, and these rules push the app bar and the content down by one slot, stacking with the
   offline/update strips rather than replacing them.

   ⚠️ Specificity is deliberate and has to be read against the block above: `html` plus the class
   adds an element and a class to each selector, so every rule here outranks its no-reconnect twin
   by one type selector at equal class count. Adding a class to a rule below without adding one here
   silently reverses that.

   ⚠️ ONE inset, always, whatever the stack: the TOP strip is the one that clears the notch, and
   below it the reconnect bar is that strip. */
html.bk-reconnect-down .mud-layout .mud-appbar {
    top: calc(var(--bk-strip-h) + var(--bk-safe-top));
    padding-top: 0;
}

html.bk-reconnect-down .mud-layout .mud-main-content {
    margin-top: calc(var(--bk-strip-h) + var(--bk-safe-top));
}

/* The other strips move down a slot rather than drawing under the reconnect bar, and they no
   longer carry the inset — the strip above them has already cleared it. */
html.bk-reconnect-down .bk-appbar-strip {
    top: calc(var(--bk-strip-h) + var(--bk-safe-top));
    padding-top: 6px;
}

html.bk-reconnect-down .bk-appbar-strip ~ .mud-layout .mud-appbar {
    top: calc(var(--bk-strip-h) * 2 + var(--bk-safe-top));
    padding-top: 0;
}

html.bk-reconnect-down .bk-appbar-strip ~ .mud-layout .mud-main-content {
    margin-top: calc(var(--bk-strip-h) * 2 + var(--bk-safe-top));
}

/* ── „Dodaj na početni ekran" card (decision 8) ──
   Non-blocking by construction: it is a card in the page flow, never an overlay and never a
   dialog. Somebody who ignores it can keep using the screen it is on. */
.bk-install-card {
    display: flex;
    align-items: flex-start;
    gap: 14px;
    width: 100%;
    padding: var(--space-4);
    margin: var(--space-3) 0;
    background: var(--surface-raised);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    text-align: left;
}

.bk-install-card__icon {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 40px;
    height: 40px;
    border-radius: var(--radius-md);
    background: var(--primary-light);
    color: var(--primary);
}

.bk-install-card__body {
    flex: 1 1 auto;
    min-width: 0;
}

.bk-install-card__title {
    font-weight: 700;
    font-size: 0.9375rem;
    color: var(--text);
    margin-bottom: var(--space-1);
}

.bk-install-card__text {
    font-size: var(--text-sm);
    color: var(--text-muted);
    line-height: 1.5;
}

.bk-install-card__steps {
    margin: var(--space-2) 0 0;
    padding-left: 18px;
    font-size: var(--text-sm);
    color: var(--text-muted);
    line-height: 1.6;
}

.bk-install-card__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-top: var(--space-3);
}

/* ── Standalone safe areas (decision 8) ──
   „standalone puni ekran + safe-area". Only in standalone: in a browser tab the chrome already
   occupies these strips, and padding them again leaves a visible dead band under the URL bar.
   The app bar and the bottom nav are the two elements the insets actually reach. */
@media all and (display-mode: standalone) {
    /* ⚠️ The pair is what makes this exact, and neither half works alone. Growing the fixed app
       bar by the top inset without moving the content down hides the first line of every page
       behind it; moving the content down without growing the bar leaves a strip of page showing
       through under the status bar.

       ⚠️ And it must NOT be done by overriding --mud-appbar-height. MudBlazor 9.1 derives
       .mud-main-content's padding-top from that variable as a FRACTION of itself — 7/8 below
       600px, 3/4 in landscape, 1/1 above — so an inset folded into the variable arrives at the
       content scaled by whichever breakpoint is live, and the gap is wrong by a few pixels in
       two of the three. Verified against the pinned 9.1.0 MudBlazor.min.css. A flat margin is
       breakpoint-agnostic and cannot drift with a MudBlazor upgrade.

       .mobile-bottom-nav already carries its own safe-area-inset-bottom (DashboardLayout), and
       it is correct in a browser tab too, so it is deliberately not repeated here. */
    .mud-appbar {
        padding-top: env(safe-area-inset-top, 0px);
    }

    .mud-main-content {
        margin-top: env(safe-area-inset-top, 0px);
    }
}

/* ══════════════════════════════════════════════════════════════════════════════════════════
   PWA step 4 — micro-interactions and the bottom bar (decisions 7 and 10)
   ══════════════════════════════════════════════════════════════════════════════════════════ */

/* ── Press feedback (decision 7: „pritisak (scale + ripple)”) ──
   ⚠️ ONE RIPPLE PER CONTROL. MudBlazor draws its own on every MudButton/MudIconButton/MudListItem,
   so a control that takes this class must ALSO set `Ripple="false"` — two ripples on one press read
   as a rendering fault, not as polish. `BkPressableCarriesOneRippleGuardTests` fails dotnet test on
   a call site that carries both, and the natural home for this class is anything MudBlazor does not
   already own: a native <a> list row, the bottom-bar items, a calendar cell.

   ⚠️ THE TRANSFORM IS TRANSIENT AND MUST STAY SO. A RETAINED transform makes an element a
   containing block, which is the documented cause of the „calendar opens but only dims the screen”
   bug (position:fixed MudBlazor pickers trapped under the backdrop) that .page-enter and .bk-route
   above both carry comments about. `:active` lasts as long as the finger and nothing here has a
   `forwards` fill, so the resting state is untouched — but do not move this onto a content wrapper.

   The ripple is CSS-only on purpose: no JS, no per-control listener, and therefore nothing to
   attach on a page the circuit has not re-rendered yet. It is centred rather than struck from the
   touch point, which is the price of having no script. */
.bk-pressable {
    position: relative;
    overflow: hidden;
    -webkit-tap-highlight-color: transparent;
    /* 2026-11-03 apple-design s1: eased release; the press is the 100ms one on :active. */
    transition: transform 160ms cubic-bezier(.32, .72, 0, 1);
}

.bk-pressable:active {
    transform: scale(0.97);
    transition: transform 100ms ease-out;
}

.bk-pressable::after {
    content: '';
    position: absolute;
    inset: 0;
    /* currentColor, so the ripple takes the control's own text colour in both themes and needs
       no token of its own. The stop pair is what makes the gradient a disc rather than a fade. */
    background-image: radial-gradient(circle, currentColor 10%, transparent 10.01%);
    background-repeat: no-repeat;
    background-position: 50%;
    transform: scale(0);
    opacity: 0;
    pointer-events: none;
    /* The RETRACTION. Short, because by this point the ripple has already said what it had to say. */
    transition: transform var(--motion-fast) var(--ease-standard), opacity var(--motion-fast) var(--ease-standard);
}

/* ⚠️ THE RIPPLE EXPANDS WHILE THE CONTROL IS HELD, NOT AFTER IT IS RELEASED — and getting this
   backwards was the whole point of the fix. The textbook CSS-only ripple rests at `scale(10)` and
   collapses to `scale(0)` on `:active`, so the visible sweep happens on the way BACK UP, i.e. on
   release. On a bar item, a list row or anything else that navigates, `:active` ends and the page
   is gone in the same frame: the one control class that most needed feedback never showed any.
   Resting at `scale(0)` and growing on `:active` puts the sweep under the finger, where it belongs.
   The disc is 10% of the box and the box is scaled 10x, so it finishes filling the control. */
.bk-pressable:active::after {
    transform: scale(10);
    opacity: 0.18;
    /* Opacity instant, size eased — that asymmetry is what reads as a ripple rather than a fade. */
    transition: transform var(--motion-slow) var(--ease-decelerate), opacity 0s;
}

/* ── Field shake on a validation error (decision 7: „tresenje polja na grešku”) ──
   ONE RULE, no per-field JS: MudBlazor stamps `mud-input-error` on the input the moment validation
   fails, and adding a class is what starts a CSS animation. Opt-in per FORM via `bk-shake` on the
   wrapper, so a page that validates as you type does not shake under the thumb.

   ⚠️ Scoped to `.mud-input`, not to `.mud-input-control`: the control is the label + input + helper
   text, and shaking the label makes the field look broken rather than wrong. No `forwards` fill, so
   the transform is gone the moment it finishes — see the containing-block note on .bk-pressable. */
.bk-shake .mud-input.mud-input-error {
    animation: bkShake var(--motion-slow) var(--ease-standard);
}

@keyframes bkShake {
    0%, 100% { transform: translate3d(0, 0, 0); }
    20% { transform: translate3d(-6px, 0, 0); }
    40% { transform: translate3d(5px, 0, 0); }
    60% { transform: translate3d(-3px, 0, 0); }
    80% { transform: translate3d(2px, 0, 0); }
}

/* ── The drawn check mark (decision 7: „kvačica koja se iscrta na uspeh”) ──
   Two dashed strokes drawn in sequence: the ring, then the tick. Colour comes from `currentColor`
   on the SVG, so Components/Shared/SuccessCheck.razor carries no colour at all and the mark takes
   whatever the confirmation panel around it is painted in. */
.bk-check__ring {
    stroke-dasharray: 166;
    stroke-dashoffset: 166;
    animation: bkCheckStroke var(--motion-slow) var(--ease-standard) forwards;
}

.bk-check__tick {
    stroke-dasharray: 48;
    stroke-dashoffset: 48;
    animation: bkCheckStroke var(--motion-base) var(--ease-decelerate) forwards;
    animation-delay: var(--motion-base);
}

@keyframes bkCheckStroke {
    to { stroke-dashoffset: 0; }
}

/* ── The bottom bar (decision 10) ──
   ⚠️ DEFINED HERE, ONCE, although `.mobile-bottom-nav` / `.mobile-nav-item` themselves are declared
   twice — in DashboardLayout's and CustomerLayout's own <style> blocks. Those two copies predate
   this file's PWA sections and are not being merged here (that is a diff across two layouts and
   every one of their responsive rules); what step 4 ADDS is written once so the staff bar and the
   portal bar cannot drift on the new behaviour. A layout <style> block renders in the BODY and
   therefore wins at equal specificity, so every selector below is deliberately compound. */

/* The selected item's icon bounces and tints (both from tokens). The tint is on the icon rather
   than only the label because at 0.65rem the label's colour change is nearly invisible on a phone. */
.mobile-nav-item.active .mud-icon-root {
    color: var(--primary);
    animation: bkNavBounce var(--motion-base) var(--ease-decelerate);
}

@keyframes bkNavBounce {
    0% { transform: translate3d(0, 0, 0) scale(1); }
    45% { transform: translate3d(0, -4px, 0) scale(1.12); }
    100% { transform: translate3d(0, 0, 0) scale(1); }
}

/* The count badge. Positioned against the item, which every bar item makes `position: relative`
   through .bk-pressable above — that class is on every one of them.

   ⚠️ THE DEFAULT IS --primary, NOT --error, and the distinction is the whole point of a badge. Red
   is the product's colour for something WRONG, and „7 termina danas” on a busy Tuesday is the
   opposite of wrong — a red 7 over Termini reads as seven problems and trains the operator to stop
   looking at the bar. Red belongs to a count somebody is being asked to CLEAR: an unread thread, a
   pending upit. Those opt in with .mobile-nav-badge--alert below. */
.mobile-nav-item .mobile-nav-badge {
    position: absolute;
    top: 2px;
    /* Half the item, plus a nudge: the icon is centred, so anchoring to the right edge puts the
       badge in the gap between items on a wide bar rather than on the glyph it counts. */
    left: calc(50% + 4px);
    /* px, not rem: the root is 14px on phones, so 0.55rem was 7.7px (audit 2026-10-15 APP-12). */
    min-width: 18px;
    height: 18px;
    padding: 0 var(--space-1);
    border-radius: var(--radius-full);
    background: var(--primary);
    color: var(--primary-btn-text);
    font-size: 10px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
    pointer-events: none;
}

/* A count that is a QUEUE — something waiting for this person to act. */
.mobile-nav-item .mobile-nav-badge--alert {
    background: var(--error);
}

/* Hide on scroll down, show on scroll up (decision 10). The modifier is added and removed by
   escalybookBottomNav in js/escalybook-interop.js.
   ⚠️ The translation is 100% of the bar PLUS its safe-area padding, or on a phone with a home
   indicator the bar parks with a strip of --surface still on screen. */
.mobile-bottom-nav.mobile-bottom-nav--hidden {
    transform: translate3d(0, calc(100% + env(safe-area-inset-bottom, 0px)), 0);
}

.mobile-bottom-nav {
    transition: transform var(--motion-base) var(--ease-standard);
    will-change: transform;
}

/* ── Reduced motion (decision 3) ──
   The global damper further up this file already clamps every animation and transition to
   0.01ms, which stops the movement. It does NOT stop an infinite shimmer from repainting, and a
   `animation-iteration-count: 1` on a 0.01ms sweep leaves the placeholder frozen at whatever
   frame it landed on. So the new classes are named explicitly and turned OFF rather than sped
   up: a flat placeholder and a bar that simply appears. */
@media (prefers-reduced-motion: reduce) {
    /* PWA step 4. Each of these is turned OFF rather than sped up, for the reason above: the
       blanket damper leaves a 0.01ms JUMP, and a check mark that snaps into existence at an
       arbitrary dash offset is worse than one that is simply drawn already. */
    .bk-pressable,
    .bk-pressable::after {
        transition: none !important;
    }

    .bk-pressable:active {
        transform: none !important;
    }

    /* The ripple still ACKNOWLEDGES the press — it just stops travelling: the tint appears at full
       size instead of sweeping out from the centre. Reduced motion asks for less movement, not for
       a control that answers a tap with nothing. ⚠️ scale(10), matching the :active rule above and
       NOT scale(1) — at scale(1) the disc is a tenth of the control, so the "acknowledgement" would
       be a dot in the middle of the button. */
    .bk-pressable:active::after {
        transform: scale(10) !important;
        opacity: 0.12;
    }

    /* A flat placeholder, not a pulse frozen at an arbitrary frame (MOT-24). */
    .mud-skeleton {
        animation: none !important;
        opacity: .6;
    }

    .bk-shake .mud-input.mud-input-error {
        animation: none !important;
        transform: none !important;
    }

    .bk-check__ring,
    .bk-check__tick {
        animation: none !important;
        stroke-dashoffset: 0 !important;
    }

    .mobile-nav-item.active .mud-icon-root {
        animation: none !important;
        transform: none !important;
    }

    /* ⚠️ ANIMATION-ONLY would be wrong here: the hide is a TRANSITION on a transform the JS still
       applies, so damping it to 0.01ms leaves the bar jumping in and out of view. The bar simply
       stays put — the scroll listener's own reduced-motion check declines to hide it at all, and
       this is the belt to that braces. */
    .mobile-bottom-nav {
        transition: none !important;
    }

    .mobile-bottom-nav.mobile-bottom-nav--hidden {
        transform: none !important;
    }

    .bk-skel {
        animation: none !important;
        background: var(--skeleton-base) !important;
    }

    .bk-navprogress {
        transition: none !important;
    }

    /* PWA step 3. ⚠️ Named explicitly rather than left to the blanket `*` damper further up this
       file: that clamps the spin to 0.01ms, which does not remove it — it freezes the ring at an
       arbitrary rotation while it keeps repainting. The splash is the only thing on screen during
       a cold standalone launch, so what is wanted is a still mark, not a stuck spinner. */
    .bk-splash__spinner {
        animation: none !important;
        border-top-color: var(--primary-light) !important;
    }

    /* PWA step 2. JS refuses to arm or play a transition under reduced motion at all, so these
       classes should never be applied — this is the belt to that braces, and it matters because
       the `*` damper above turns a 220ms slide into a 0.01ms JUMP rather than removing it. The
       page must simply be there. */
    .bk-route--enter-forward,
    .bk-route--enter-backward,
    .bk-route--enter-lateral {
        /* 2026-11-03 apple-design s14: no `opacity: 1 !important` here any more, it would beat the
           150ms fade (bkFadeOnly) that the end of this file gives these classes. The slide is still
           removed by that block's animation-name swap, and no transform is ever set. */
        transform: none !important;
    }

    /* ⚠️ ANIMATION ONLY — no `transform: none`. The drag-down gesture writes an inline
       `transform: translate3d(0, Npx, 0)` as the finger moves, and a rule with !important beats an
       inline declaration: the sheet would refuse to follow the thumb, and someone who had asked
       for less motion would be left with a sheet that cannot be dragged at all. Reduced motion
       asks for no gratuitous MOVEMENT, not for no direct manipulation. */
    .mud-dialog-container.mud-dialog-bottomcenter .mud-dialog {
        animation: none !important;
    }

    .bk-navprogress--active .bk-navprogress__bar {
        animation: none !important;
        transform: none !important;
        width: 100% !important;
    }

    /* ⚠️ THE ONE EXEMPTION, and it is deliberate. The blanket `*` damper further up this file
       clamps every animation to 0.01ms, which stops the busy button's spinner dead — leaving a
       disabled button with a frozen ring beside a label, and nothing on the screen saying the
       press was received. Reduced motion asks for less DECORATIVE movement; this is the only
       feedback a pressed button has, and there is no static substitute for it (the label stays,
       the button does not resize, so nothing else changes at all). Named narrowly, on the slot
       only, so it cannot become a way back in for anything else. The child selector is required:
       MudBlazor animates the SVG circle inside, not the wrapper. */
    .bk-busy-btn__slot .mud-progress-circular,
    .bk-busy-btn__slot .mud-progress-circular * {
        animation-duration: 1.4s !important;
        animation-iteration-count: infinite !important;
    }
}
/* Platform administration: phone composition, scoped away from tenant dashboards. */
.admin-shell button,
.admin-shell a,
.admin-shell select {
    touch-action: manipulation;
}

.admin-shell .mobile-bottom-nav {
    gap: var(--space-1);
}

.admin-shell .mobile-nav-item {
    flex: 1 1 0;
    min-width: 0;
}

.admin-shell .mobile-nav-item:focus-visible,
.admin-workspace select:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

.admin-more-sheet .mud-drawer-content {
    overscroll-behavior-y: contain;
}

.admin-workspace,
.admin-workspace .mud-container,
.admin-workspace .mud-grid-item {
    min-width: 0;
}

.admin-workspace .admin-money,
.admin-dialog-content .admin-money {
    font-variant-numeric: tabular-nums;
}

.admin-workspace .admin-filter-fields {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-2);
    min-width: 0;
}

@media (max-width: 599.98px) {
    .admin-workspace .mud-typography-h4 {
        font-size: var(--text-3xl);
        line-height: 1.25;
        overflow-wrap: anywhere;
    }

    .admin-workspace .mud-button-root,
    .admin-workspace .mud-icon-button,
    .admin-more-sheet .mud-nav-link {
        min-height: 44px;
    }

    .admin-workspace .mud-icon-button {
        min-width: 44px;
    }

    .admin-workspace input,
    .admin-workspace textarea,
    .admin-workspace select {
        font-size: 16px;
    }

    .admin-workspace .mud-input:not(.mud-input-textarea),
    .admin-dialog-content .mud-input:not(.mud-input-textarea) {
        min-height: 48px;
    }

    .admin-workspace .admin-page-header {
        display: flex;
        flex-direction: column;
        align-items: stretch !important;
        gap: var(--space-3);
    }

    .admin-workspace .admin-page-header > * {
        min-width: 0;
        max-width: 100%;
    }

    .admin-workspace .admin-page-header .mud-typography-h4 {
        font-size: var(--text-3xl);
        overflow-wrap: anywhere;
    }

    .admin-workspace .admin-page-header .mud-button-label {
        white-space: normal;
    }

    .admin-workspace .admin-page-header.admin-icon-header {
        display: grid !important;
        grid-template-columns: minmax(0, 1fr) 44px;
        align-items: start !important;
    }

    .admin-workspace .admin-templates-page {
        padding: 0 !important;
    }

    .admin-workspace .admin-filter-bar {
        display: flex;
        flex-direction: column;
        align-items: stretch !important;
        gap: var(--space-3);
        max-width: none !important;
        width: 100%;
    }

    .admin-workspace .admin-filter-bar > *,
    .admin-workspace .admin-mobile-panel .d-flex > *,
    .admin-workspace .mud-input-control,
    .admin-dialog-content .mud-input-control {
        min-width: 0 !important;
        max-width: 100%;
    }

    /* Trap: in a column flexbox flex-basis is the HEIGHT. Desktop inline values such as
       flex:1 1 260px or flex:1 1 220px would become a 260px or 220px blank gap on the
       phone, so the basis is reset to auto here (grow and shrink stay as authored).
       Only the two column containers: a dialog input keeps whatever basis it was given. */
    .admin-workspace .admin-filter-bar > *,
    .admin-workspace .admin-mobile-panel .d-flex > * {
        flex-basis: auto !important;
    }

    /* F13: the template header (icon, name, status) is the one child that keeps a full row, so the
       channel chip and the actions wrap underneath. It must follow the generic reset above. */
    .admin-workspace .admin-mobile-panel .d-flex > .pt-head {
        flex-basis: 100% !important;
    }

    .admin-workspace .admin-filter-bar .mud-picker,
    .admin-workspace .admin-filter-bar .mud-picker .mud-input-control {
        width: 100%;
        max-width: none !important;
        min-width: 0 !important;
    }

    /* Two pickers grouped in .admin-filter-fields share ONE row (Izdato od / Izdato do): a
       shrinkable equal basis instead of the full width a standalone picker gets above.
       (Codex, blazor#760.) */
    .admin-workspace .admin-filter-fields .mud-picker {
        flex: 1 1 0;
        width: auto;
        min-width: 0 !important;
        max-width: none !important;
    }

    .admin-workspace .mobile-card-list .action-cluster {
        display: flex;
        flex-wrap: nowrap;
        gap: var(--space-1);
    }

    .admin-workspace .mobile-card-list .action-cluster > .mud-button-root {
        flex: 1 1 0;
        min-width: 0;
        padding-left: var(--space-1);
        padding-right: var(--space-1);
    }

    .admin-workspace .mobile-card-list .action-cluster .mud-button-label {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    .admin-workspace .admin-filter-bar > div {
        min-width: 0 !important;
        width: 100%;
    }

    .admin-workspace .admin-filter-bar > .mud-input-control,
    .admin-workspace .admin-filter-bar > .admin-filter-fields,
    .admin-workspace .admin-filter-fields > .mud-input-control {
        width: 100%;
        max-width: none !important;
        flex: 1 1 auto !important;
    }

    .admin-workspace .admin-filter-fields,
    .admin-workspace .admin-inline-actions {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-2);
        min-width: 0;
        max-width: 100%;
    }

    .admin-workspace .admin-inline-actions > * {
        min-width: 0;
        max-width: 100%;
    }

    .admin-workspace .admin-inline-actions .mud-button-label {
        white-space: normal;
    }

    .admin-workspace .admin-metric-grid {
        grid-template-columns: minmax(0, 1fr) !important;
        gap: var(--space-3) !important;
    }

    .admin-workspace .admin-metric-grid > * {
        min-width: 0;
        overflow-wrap: anywhere;
    }

    /* A list owns its rows. An extra paper frame costs two phone gutters. */
    .admin-workspace .admin-flat-list {
        padding: 0 !important;
        border: 0 !important;
        border-radius: 0 !important;
        background: transparent !important;
        box-shadow: none !important;
    }

    .admin-workspace .admin-responsive-table {
        background: transparent !important;
        box-shadow: none !important;
        border: 0 !important;
        border-radius: 0 !important;
    }

    .admin-workspace .admin-responsive-table tbody tr {
        padding: var(--space-3) 0;
        margin: 0;
        border: 0;
        border-bottom: 1px solid var(--border-light);
        border-radius: 0;
        background: transparent;
    }

    .admin-workspace .admin-responsive-table tbody td[data-label]::before {
        font-size: var(--text-sm);
        font-weight: 400;
        letter-spacing: normal;
        text-transform: none;
    }

    .admin-workspace .admin-responsive-table tbody td:first-child {
        display: block !important;
        text-align: left !important;
        max-width: none !important;
    }

    .admin-workspace .admin-responsive-table tbody td:first-child::before {
        display: block;
        margin-bottom: var(--space-1);
    }

    .admin-workspace .admin-broadcast-summary > * {
        white-space: normal !important;
        max-width: none !important;
        overflow-wrap: anywhere;
    }

    .admin-workspace .wa-thread-header {
        display: grid !important;
        grid-template-columns: 44px minmax(0, 1fr);
        gap: var(--space-2) !important;
    }

    .admin-workspace .wa-thread-contact {
        min-width: 0;
        overflow-wrap: anywhere;
    }

    .admin-workspace .wa-thread-status {
        grid-column: 2;
        justify-self: start;
        max-width: 100%;
    }

    .admin-workspace .admin-template-row {
        flex-wrap: wrap;
        gap: var(--space-2) var(--space-3);
    }

    /* F13: the content sits BESIDE the 36px icon (one line: icon, name, status), not on a row of its own;
       the channel chip, the penalty chips and the actions wrap underneath. */
    .admin-workspace .pt-head {
        flex: 1 1 100%;
    }

    .admin-workspace .admin-template-content {
        flex: 1 1 0 !important;
        min-width: 0;
        overflow-wrap: anywhere;
    }

    .admin-workspace .admin-ticket-message {
        max-width: 100% !important;
        overflow-wrap: anywhere;
    }

    .admin-workspace .admin-onboarding-surface .mud-alert-message {
        min-width: 0;
        overflow-wrap: anywhere;
    }

    .mud-dialog:has(.admin-dialog-content) .mud-button-root,
    .mud-dialog:has(.admin-dialog-content) .mud-icon-button {
        min-height: 44px;
    }

    .mud-dialog:has(.admin-dialog-content) .mud-icon-button {
        min-width: 44px;
    }

    .admin-dialog-content input,
    .admin-dialog-content textarea,
    .admin-dialog-content select {
        font-size: 16px;
    }

    .admin-workspace .admin-mobile-panel {
        padding: var(--space-4) !important;
    }

    .admin-workspace .admin-financial-summary,
    .admin-dialog-content .admin-financial-summary {
        grid-template-columns: minmax(0, 1fr) !important;
    }

    .admin-workspace .bl-table-cards tbody td {
        min-width: 0;
        overflow-wrap: anywhere;
        align-items: flex-start;
    }

    .admin-workspace .bl-table-cards tbody td[data-label]::before {
        flex: 0 0 38%;
        min-width: 0;
        white-space: normal;
        overflow-wrap: anywhere;
    }

    .admin-workspace .bl-table-cards tbody td > * {
        min-width: 0;
        max-width: 100%;
    }

    .admin-dialog-content {
        min-width: 0 !important;
        max-width: 100%;
        overflow-wrap: anywhere;
        max-height: none !important;
    }

    .admin-workspace .admin-collateral-list > div {
        flex-wrap: wrap;
    }

    .admin-workspace .admin-collateral-list > div > * {
        min-width: 0;
        max-width: 100%;
    }
}

/* Paket dolazaka (MembershipCard) in the klijent dialog. */
.mpc-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.mpc-card {
    border: 1px solid var(--border-light);
    border-radius: var(--radius-md);
    padding: 12px var(--space-3);
}

.mpc-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
}

.mpc-top .bk-status-chip {
    flex-shrink: 0;
}

.mpc-name {
    min-width: 0;
    font-weight: 600;
    font-size: var(--text-base);
    color: var(--text);
    overflow-wrap: anywhere;
}

.mpc-service {
    font-size: var(--text-xs);
    color: var(--text-secondary);
    margin-top: 2px;
}

.mpc-track {
    width: 100%;
    margin: 8px 0 4px;
}

.mpc-meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-3);
    font-size: var(--text-xs);
    color: var(--text-muted);
}

/* ── The analytics „i” (AnalyticsInfo.razor). The box is 20 px so a row keeps its width; the 44 px
   tap target is the ::after, which lays nothing out. .ai-roomy is for a place with a tappable
   neighbour (a sort button): it gives the target its own room. The tip renders in the popover
   provider, so its type is set here rather than inherited from an uppercase label. ── */
.ai-anchor {
    position: relative;
    display: inline-flex;
    vertical-align: middle;
    margin-inline-start: 2px;
    text-transform: none;
    letter-spacing: 0;
}

.ai-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-full);
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
}

.ai-btn::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}

.ai-btn:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 1px;
}

.ai-btn .mud-icon-root {
    font-size: 16px;
}

.ai-roomy {
    display: inline-flex;
    margin-inline: 12px;
}

/* !important: MudBlazor's popover script writes max-width on the element when it places it. */
.ai-tip {
    max-width: 280px !important;
    padding: var(--space-2) var(--space-3);
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    font-weight: 400;
    line-height: 1.4;
}

/* On a phone the tip is the width of the page's 16 px gutters, wherever the „i” sits. */
@media (max-width: 599px) {
    .mud-popover.ai-tip {
        left: 16px !important;
        right: 16px;
        max-width: none !important;
    }
}

/* The + menu (DashboardFab.razor): 48 px rows, over the 44 px floor. */
.dashboard-fab-list .mud-menu-item {
    min-height: 48px;
}

/* Pretplata, "Promeni period placanja" (owner, 2026-11-01): the cycle picker, the plan cards and the
   action row hold at 320 px. BillingCyclePicker.razor, SubscriptionPage.razor. */
.cycle-picker {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-2);
}
.cycle-picker-option {
    box-sizing: border-box;
    min-width: 0;
    min-height: var(--touch-target-min);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 0 var(--space-2);
    padding: var(--space-2) var(--space-3);
    font: inherit;
    font-size: var(--text-sm);
    font-weight: 600;
    text-align: center;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
}
.cycle-picker-option--selected {
    color: var(--primary);
    background: var(--primary-light);
    border-color: var(--primary);
}
.cycle-picker-discount {
    font-size: var(--text-xs);
    font-weight: 500;
    color: var(--text-muted);
}
@media (min-width: 600px) {
    .cycle-picker { grid-template-columns: repeat(4, max-content); }
}

.plan-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
    gap: var(--space-3);
}
.plan-option {
    box-sizing: border-box;
    min-width: 0;
    max-width: 100%;
    position: relative;
}
.plan-option-badge {
    display: inline-block;
    margin-bottom: var(--space-2);
}
.plan-actions {
    display: flex;
    gap: var(--space-3);
    margin-top: var(--space-4);
    flex-direction: column;
}
.plan-actions > .mud-button-root {
    width: 100%;
    min-height: var(--touch-target-min);
    white-space: normal;
}
@media (min-width: 600px) {
    .plan-option-badge { position: absolute; top: 8px; right: 8px; margin-bottom: 0; }
    .plan-actions { flex-direction: row; }
    .plan-actions > .mud-button-root { width: auto; }
}

/* ── Reduced motion: gentle replacement (2026-11-03 apple-design s14) ──
   Reduced motion means a non-vestibular equivalent, not nothing. Dialogs, route changes and the
   snackbar get a 150ms opacity-only fade (no slide, no scale). The earlier reduced-motion blocks
   in this file no longer list .mud-dialog or .mud-snackbar (their `animation: none !important` and
   `opacity: 1 !important` would beat this fade), and the `*` damper above is out-ranked here by
   specificity plus an explicit animation-duration. Everything else stays damped.
   ⚠️ ANIMATION ONLY, no `transform: none`: the bottom-sheet drag writes an inline transform. */
@keyframes bkFadeOnly {
    from { opacity: 0; }
    to { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    /* Entering state only: any leaving/closing class MudBlazor adds keeps its own animation. MudBlazor
       9.1.0's CSS has no such class for dialog or snackbar (checked in the NuGet staticwebassets),
       so the :not() guards are cheap insurance for a later version. */
    .mud-dialog:not([class*="leav"]):not([class*="clos"]),
    .mud-dialog-container.mud-dialog-bottomcenter .mud-dialog:not([class*="leav"]):not([class*="clos"]),
    .mud-snackbar:not([class*="leav"]):not([class*="clos"]),
    .bk-route.bk-route--enter-forward,
    .bk-route.bk-route--enter-backward,
    .bk-route.bk-route--enter-lateral {
        animation-name: bkFadeOnly !important;
        animation-duration: 150ms !important;
        animation-timing-function: ease-out !important;
        animation-iteration-count: 1 !important;
        animation-fill-mode: both !important;
    }
}

/* ── Numbers (2026-11-03 apple-design s15) ──
   Times, prices and counts in the staff dashboard line up in columns when they are tabular.
   Short list of the existing classes, not `body`. */
.bk-num,
.ag-start,
.ag-end,
.ag-now-time,
.um-meta,
.wc-time-label,
.wc-booking-inline,
.dbe-hour,
.kpi-value,
.kpi-caption,
.pl-cell-value,
.pl-cell-margin,
.billing-amount {
    font-variant-numeric: tabular-nums;
}
